import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
import { a as isSqlite, n as currentTimestamp, r as currentTimestampValue, s as listTablesLike, t as binaryType } from "./dialect-helpers-DhTzaUxP.mjs";
import { Migrator, sql } from "kysely";

//#region src/database/migrations/001_initial.ts
var _001_initial_exports = /* @__PURE__ */ __exportAll({
	down: () => down$32,
	up: () => up$32
});
/**
* Initial schema migration
*
* Note: Content tables (ec_posts, ec_pages, etc.) are created dynamically
* by the SchemaRegistry when collections are added via the admin UI.
* This migration only creates system tables.
*/
async function up$32(db) {
	await db.schema.createTable("revisions").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection", "text", (col) => col.notNull()).addColumn("entry_id", "text", (col) => col.notNull()).addColumn("data", "text", (col) => col.notNull()).addColumn("author_id", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createIndex("idx_revisions_entry").ifNotExists().on("revisions").columns(["collection", "entry_id"]).execute();
	await db.schema.createTable("taxonomies").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).addColumn("slug", "text", (col) => col.notNull()).addColumn("label", "text", (col) => col.notNull()).addColumn("parent_id", "text").addColumn("data", "text").addUniqueConstraint("taxonomies_name_slug_unique", ["name", "slug"]).addForeignKeyConstraint("taxonomies_parent_fk", ["parent_id"], "taxonomies", ["id"], (cb) => cb.onDelete("set null")).execute();
	await db.schema.createIndex("idx_taxonomies_name").ifNotExists().on("taxonomies").column("name").execute();
	await db.schema.createTable("content_taxonomies").ifNotExists().addColumn("collection", "text", (col) => col.notNull()).addColumn("entry_id", "text", (col) => col.notNull()).addColumn("taxonomy_id", "text", (col) => col.notNull()).addPrimaryKeyConstraint("content_taxonomies_pk", [
		"collection",
		"entry_id",
		"taxonomy_id"
	]).addForeignKeyConstraint("content_taxonomies_taxonomy_fk", ["taxonomy_id"], "taxonomies", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createTable("media").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("filename", "text", (col) => col.notNull()).addColumn("mime_type", "text", (col) => col.notNull()).addColumn("size", "integer").addColumn("width", "integer").addColumn("height", "integer").addColumn("alt", "text").addColumn("caption", "text").addColumn("storage_key", "text", (col) => col.notNull()).addColumn("content_hash", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("author_id", "text").execute();
	await db.schema.createIndex("idx_media_content_hash").ifNotExists().on("media").column("content_hash").execute();
	await db.schema.createTable("users").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("password_hash", "text", (col) => col.notNull()).addColumn("name", "text").addColumn("role", "text", (col) => col.defaultTo("subscriber")).addColumn("avatar_id", "text").addColumn("data", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createIndex("idx_users_email").ifNotExists().on("users").column("email").execute();
	await db.schema.createTable("options").ifNotExists().addColumn("name", "text", (col) => col.primaryKey()).addColumn("value", "text", (col) => col.notNull()).execute();
	await db.schema.createTable("audit_logs").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("timestamp", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("actor_id", "text").addColumn("actor_ip", "text").addColumn("action", "text", (col) => col.notNull()).addColumn("resource_type", "text").addColumn("resource_id", "text").addColumn("details", "text").addColumn("status", "text").execute();
	await db.schema.createIndex("idx_audit_actor").ifNotExists().on("audit_logs").column("actor_id").execute();
	await db.schema.createIndex("idx_audit_action").ifNotExists().on("audit_logs").column("action").execute();
	await db.schema.createIndex("idx_audit_timestamp").ifNotExists().on("audit_logs").column("timestamp").execute();
}
async function down$32(db) {
	await db.schema.dropTable("audit_logs").execute();
	await db.schema.dropTable("options").execute();
	await db.schema.dropTable("users").execute();
	await db.schema.dropTable("media").execute();
	await db.schema.dropTable("content_taxonomies").execute();
	await db.schema.dropTable("taxonomies").execute();
	await db.schema.dropTable("revisions").execute();
}

//#endregion
//#region src/database/migrations/002_media_status.ts
var _002_media_status_exports = /* @__PURE__ */ __exportAll({
	down: () => down$31,
	up: () => up$31
});
/**
* Add status column to media table for tracking upload state.
* Status values: 'pending' | 'ready' | 'failed'
*/
async function up$31(db) {
	await db.schema.alterTable("media").addColumn("status", "text", (col) => col.notNull().defaultTo("ready")).execute();
	await db.schema.createIndex("idx_media_status").on("media").column("status").execute();
}
async function down$31(db) {
	await db.schema.dropIndex("idx_media_status").execute();
}

//#endregion
//#region src/database/migrations/003_schema_registry.ts
var _003_schema_registry_exports = /* @__PURE__ */ __exportAll({
	down: () => down$30,
	up: () => up$30
});
/**
* Migration: Schema Registry Tables
*
* Creates the schema registry tables that store collection and field definitions.
* This enables dynamic schema management where D1 is the source of truth.
*/
async function up$30(db) {
	await db.schema.createTable("_emdash_collections").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("label_singular", "text").addColumn("description", "text").addColumn("icon", "text").addColumn("supports", "text").addColumn("source", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createTable("_emdash_fields").addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("slug", "text", (col) => col.notNull()).addColumn("label", "text", (col) => col.notNull()).addColumn("type", "text", (col) => col.notNull()).addColumn("column_type", "text", (col) => col.notNull()).addColumn("required", "integer", (col) => col.defaultTo(0)).addColumn("unique", "integer", (col) => col.defaultTo(0)).addColumn("default_value", "text").addColumn("validation", "text").addColumn("widget", "text").addColumn("options", "text").addColumn("sort_order", "integer", (col) => col.defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("fields_collection_fk", ["collection_id"], "_emdash_collections", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createIndex("idx_fields_collection_slug").on("_emdash_fields").columns(["collection_id", "slug"]).unique().execute();
	await db.schema.createIndex("idx_fields_collection").on("_emdash_fields").column("collection_id").execute();
	await db.schema.createIndex("idx_fields_sort").on("_emdash_fields").columns(["collection_id", "sort_order"]).execute();
}
async function down$30(db) {
	await db.schema.dropTable("_emdash_fields").execute();
	await db.schema.dropTable("_emdash_collections").execute();
}

//#endregion
//#region src/database/migrations/004_plugins.ts
var _004_plugins_exports = /* @__PURE__ */ __exportAll({
	down: () => down$29,
	up: () => up$29
});
/**
* Migration: Plugin System Tables
*
* Creates the plugin storage table and plugin state tracking.
* Plugin storage uses a document store with declared indexes.
*
* @see PLUGIN-SYSTEM.md § Plugin Storage
*/
async function up$29(db) {
	await db.schema.createTable("_plugin_storage").addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("collection", "text", (col) => col.notNull()).addColumn("id", "text", (col) => col.notNull()).addColumn("data", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("pk_plugin_storage", [
		"plugin_id",
		"collection",
		"id"
	]).execute();
	await db.schema.createIndex("idx_plugin_storage_list").on("_plugin_storage").columns([
		"plugin_id",
		"collection",
		"created_at"
	]).execute();
	await db.schema.createTable("_plugin_state").addColumn("plugin_id", "text", (col) => col.primaryKey()).addColumn("version", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("installed")).addColumn("installed_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("activated_at", "text").addColumn("deactivated_at", "text").addColumn("data", "text").execute();
	await db.schema.createTable("_plugin_indexes").addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("collection", "text", (col) => col.notNull()).addColumn("index_name", "text", (col) => col.notNull()).addColumn("fields", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("pk_plugin_indexes", [
		"plugin_id",
		"collection",
		"index_name"
	]).execute();
}
async function down$29(db) {
	await db.schema.dropTable("_plugin_indexes").execute();
	await db.schema.dropTable("_plugin_state").execute();
	await db.schema.dropTable("_plugin_storage").execute();
}

//#endregion
//#region src/database/migrations/005_menus.ts
var _005_menus_exports = /* @__PURE__ */ __exportAll({
	down: () => down$28,
	up: () => up$28
});
/**
* Navigation Menus migration
*
* Creates tables for admin-editable navigation menus.
* Menu items can reference content entries, taxonomy terms, or custom URLs.
*/
async function up$28(db) {
	await db.schema.createTable("_emdash_menus").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createTable("_emdash_menu_items").addColumn("id", "text", (col) => col.primaryKey()).addColumn("menu_id", "text", (col) => col.notNull()).addColumn("parent_id", "text").addColumn("sort_order", "integer", (col) => col.notNull().defaultTo(0)).addColumn("type", "text", (col) => col.notNull()).addColumn("reference_collection", "text").addColumn("reference_id", "text").addColumn("custom_url", "text").addColumn("label", "text", (col) => col.notNull()).addColumn("title_attr", "text").addColumn("target", "text").addColumn("css_classes", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("menu_items_menu_fk", ["menu_id"], "_emdash_menus", ["id"], (cb) => cb.onDelete("cascade")).addForeignKeyConstraint("menu_items_parent_fk", ["parent_id"], "_emdash_menu_items", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createIndex("idx_menu_items_menu").on("_emdash_menu_items").columns(["menu_id", "sort_order"]).execute();
	await db.schema.createIndex("idx_menu_items_parent").on("_emdash_menu_items").column("parent_id").execute();
}
async function down$28(db) {
	await db.schema.dropTable("_emdash_menu_items").execute();
	await db.schema.dropTable("_emdash_menus").execute();
}

//#endregion
//#region src/database/migrations/006_taxonomy_defs.ts
var _006_taxonomy_defs_exports = /* @__PURE__ */ __exportAll({
	down: () => down$27,
	up: () => up$27
});
/**
* Taxonomy definitions migration
*
* Adds _emdash_taxonomy_defs table to store taxonomy definitions (category, tag, custom)
* and seeds default category and tag taxonomies.
*/
async function up$27(db) {
	await db.schema.createTable("_emdash_taxonomy_defs").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("label_singular", "text").addColumn("hierarchical", "integer", (col) => col.defaultTo(0)).addColumn("collections", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.insertInto("_emdash_taxonomy_defs").values([{
		id: "taxdef_category",
		name: "category",
		label: "Categories",
		label_singular: "Category",
		hierarchical: 1,
		collections: JSON.stringify(["posts"])
	}, {
		id: "taxdef_tag",
		name: "tag",
		label: "Tags",
		label_singular: "Tag",
		hierarchical: 0,
		collections: JSON.stringify(["posts"])
	}]).execute();
}
async function down$27(db) {
	await db.schema.dropTable("_emdash_taxonomy_defs").execute();
}

//#endregion
//#region src/database/migrations/007_widgets.ts
var _007_widgets_exports = /* @__PURE__ */ __exportAll({
	down: () => down$26,
	up: () => up$26
});
async function up$26(db) {
	await db.schema.createTable("_emdash_widget_areas").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("description", "text").addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
	await db.schema.createTable("_emdash_widgets").addColumn("id", "text", (col) => col.primaryKey()).addColumn("area_id", "text", (col) => col.notNull().references("_emdash_widget_areas.id").onDelete("cascade")).addColumn("sort_order", "integer", (col) => col.notNull().defaultTo(0)).addColumn("type", "text", (col) => col.notNull()).addColumn("title", "text").addColumn("content", "text").addColumn("menu_name", "text").addColumn("component_id", "text").addColumn("component_props", "text").addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
	await db.schema.createIndex("idx_widgets_area").on("_emdash_widgets").columns(["area_id", "sort_order"]).execute();
}
async function down$26(db) {
	await db.schema.dropTable("_emdash_widgets").execute();
	await db.schema.dropTable("_emdash_widget_areas").execute();
}

//#endregion
//#region src/database/migrations/008_auth.ts
var _008_auth_exports = /* @__PURE__ */ __exportAll({
	down: () => down$25,
	up: () => up$25
});
/**
* Auth migration - passkey-first authentication
*
* Changes:
* - Removes password_hash from users (no passwords)
* - Adds role as integer (RBAC levels)
* - Adds email_verified, avatar_url, updated_at to users
* - Creates credentials table (passkeys)
* - Creates auth_tokens table (magic links, invites)
* - Creates oauth_accounts table (external provider links)
* - Creates allowed_domains table (self-signup)
*/
async function up$25(db) {
	await db.schema.createTable("users_new").addColumn("id", "text", (col) => col.primaryKey()).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("name", "text").addColumn("avatar_url", "text").addColumn("role", "integer", (col) => col.notNull().defaultTo(10)).addColumn("email_verified", "integer", (col) => col.notNull().defaultTo(0)).addColumn("data", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await sql`
		INSERT INTO users_new (id, email, name, role, data, created_at, updated_at)
		SELECT
			id,
			email,
			name,
			CASE role
				WHEN 'admin' THEN 50
				WHEN 'editor' THEN 40
				WHEN 'author' THEN 30
				WHEN 'contributor' THEN 20
				ELSE 10
			END,
			data,
			created_at,
			${currentTimestampValue(db)}
		FROM users
	`.execute(db);
	await db.schema.dropTable("users").execute();
	await sql`ALTER TABLE users_new RENAME TO users`.execute(db);
	await db.schema.createIndex("idx_users_email").on("users").column("email").execute();
	await db.schema.createTable("credentials").addColumn("id", "text", (col) => col.primaryKey()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("public_key", binaryType(db), (col) => col.notNull()).addColumn("counter", "integer", (col) => col.notNull().defaultTo(0)).addColumn("device_type", "text", (col) => col.notNull()).addColumn("backed_up", "integer", (col) => col.notNull().defaultTo(0)).addColumn("transports", "text").addColumn("name", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("last_used_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("credentials_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createIndex("idx_credentials_user").on("credentials").column("user_id").execute();
	await db.schema.createTable("auth_tokens").addColumn("hash", "text", (col) => col.primaryKey()).addColumn("user_id", "text").addColumn("email", "text").addColumn("type", "text", (col) => col.notNull()).addColumn("role", "integer").addColumn("invited_by", "text").addColumn("expires_at", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("auth_tokens_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).addForeignKeyConstraint("auth_tokens_invited_by_fk", ["invited_by"], "users", ["id"], (cb) => cb.onDelete("set null")).execute();
	await db.schema.createIndex("idx_auth_tokens_email").on("auth_tokens").column("email").execute();
	await db.schema.createTable("oauth_accounts").addColumn("provider", "text", (col) => col.notNull()).addColumn("provider_account_id", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("oauth_accounts_pk", ["provider", "provider_account_id"]).addForeignKeyConstraint("oauth_accounts_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createIndex("idx_oauth_accounts_user").on("oauth_accounts").column("user_id").execute();
	await db.schema.createTable("allowed_domains").addColumn("domain", "text", (col) => col.primaryKey()).addColumn("default_role", "integer", (col) => col.notNull().defaultTo(20)).addColumn("enabled", "integer", (col) => col.notNull().defaultTo(1)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createTable("auth_challenges").addColumn("challenge", "text", (col) => col.primaryKey()).addColumn("type", "text", (col) => col.notNull()).addColumn("user_id", "text").addColumn("data", "text").addColumn("expires_at", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createIndex("idx_auth_challenges_expires").on("auth_challenges").column("expires_at").execute();
}
async function down$25(db) {
	await db.schema.dropTable("auth_challenges").execute();
	await db.schema.dropTable("allowed_domains").execute();
	await db.schema.dropTable("oauth_accounts").execute();
	await db.schema.dropTable("auth_tokens").execute();
	await db.schema.dropTable("credentials").execute();
	await db.schema.createTable("users_old").addColumn("id", "text", (col) => col.primaryKey()).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("password_hash", "text", (col) => col.notNull()).addColumn("name", "text").addColumn("role", "text", (col) => col.defaultTo("subscriber")).addColumn("avatar_id", "text").addColumn("data", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await sql`
		INSERT INTO users_old (id, email, password_hash, name, role, data, created_at)
		SELECT
			id,
			email,
			'', -- No way to restore password
			name,
			CASE role
				WHEN 50 THEN 'admin'
				WHEN 40 THEN 'editor'
				WHEN 30 THEN 'author'
				WHEN 20 THEN 'contributor'
				ELSE 'subscriber'
			END,
			data,
			created_at
		FROM users
	`.execute(db);
	await db.schema.dropTable("users").execute();
	await sql`ALTER TABLE users_old RENAME TO users`.execute(db);
	await db.schema.createIndex("idx_users_email").on("users").column("email").execute();
}

//#endregion
//#region src/database/migrations/009_user_disabled.ts
var _009_user_disabled_exports = /* @__PURE__ */ __exportAll({
	down: () => down$24,
	up: () => up$24
});
/**
* User disabled column - for soft-disabling users
*
* Changes:
* - Adds disabled column to users table (INTEGER, default 0)
* - Disabled users cannot log in
*/
async function up$24(db) {
	await sql`ALTER TABLE users ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0`.execute(db);
	await db.schema.createIndex("idx_users_disabled").on("users").column("disabled").execute();
}
async function down$24(db) {
	await db.schema.dropIndex("idx_users_disabled").execute();
}

//#endregion
//#region src/database/migrations/011_sections.ts
var _011_sections_exports = /* @__PURE__ */ __exportAll({
	down: () => down$23,
	up: () => up$23
});
/**
* Migration: Add sections tables and performance indexes
*
* Sections are reusable content blocks that can be inserted into any Portable Text field.
* They provide a library of pre-built page sections (heroes, CTAs, testimonials, etc.)
* that content authors can browse and insert with a single click.
*/
async function up$23(db) {
	await db.schema.createTable("_emdash_section_categories").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("sort_order", "integer", (col) => col.defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
	await db.schema.createTable("_emdash_sections").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("title", "text", (col) => col.notNull()).addColumn("description", "text").addColumn("category_id", "text", (col) => col.references("_emdash_section_categories.id").onDelete("set null")).addColumn("keywords", "text").addColumn("content", "text", (col) => col.notNull()).addColumn("preview_media_id", "text").addColumn("source", "text", (col) => col.notNull().defaultTo("user")).addColumn("theme_id", "text").addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).addColumn("updated_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
	await db.schema.createIndex("idx_sections_category").on("_emdash_sections").columns(["category_id"]).execute();
	await db.schema.createIndex("idx_sections_source").on("_emdash_sections").columns(["source"]).execute();
}
async function down$23(db) {
	await db.schema.dropIndex("idx_sections_source").execute();
	await db.schema.dropIndex("idx_sections_category").execute();
	await db.schema.dropTable("_emdash_sections").execute();
	await db.schema.dropTable("_emdash_section_categories").execute();
}

//#endregion
//#region src/database/migrations/012_search.ts
var _012_search_exports = /* @__PURE__ */ __exportAll({
	down: () => down$22,
	up: () => up$22
});
/**
* Migration: Search Support
*
* Adds search configuration to collections and searchable flag to fields.
* FTS5 tables are created dynamically when search is enabled for a collection.
*/
async function up$22(db) {
	await db.schema.alterTable("_emdash_collections").addColumn("search_config", "text").execute();
	await db.schema.alterTable("_emdash_fields").addColumn("searchable", "integer", (col) => col.defaultTo(0)).execute();
}
async function down$22(db) {
	await db.schema.alterTable("_emdash_fields").dropColumn("searchable").execute();
	await db.schema.alterTable("_emdash_collections").dropColumn("search_config").execute();
}

//#endregion
//#region src/database/migrations/013_scheduled_publishing.ts
var _013_scheduled_publishing_exports = /* @__PURE__ */ __exportAll({
	down: () => down$21,
	up: () => up$21
});
/**
* Migration: Add scheduled publishing support
*
* Adds scheduled_at column to all ec_* content tables.
* When scheduled_at is set and status is 'scheduled', the content
* will be auto-published when the scheduled time is reached.
*/
async function up$21(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`
			ALTER TABLE ${sql.ref(table.name)} 
			ADD COLUMN scheduled_at TEXT
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_scheduled`)} 
			ON ${sql.ref(table.name)} (scheduled_at)
			WHERE scheduled_at IS NOT NULL AND status = 'scheduled'
		`.execute(db);
	}
}
async function down$21(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`
			DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_scheduled`)}
		`.execute(db);
		await sql`
			ALTER TABLE ${sql.ref(table.name)} 
			DROP COLUMN scheduled_at
		`.execute(db);
	}
}

//#endregion
//#region src/database/migrations/014_draft_revisions.ts
var _014_draft_revisions_exports = /* @__PURE__ */ __exportAll({
	down: () => down$20,
	up: () => up$20
});
async function up$20(db) {
	const tables = await db.selectFrom("_emdash_collections").select("slug").execute();
	for (const row of tables) {
		const tableName = `ec_${row.slug}`;
		await sql`
			ALTER TABLE ${sql.ref(tableName)}
			ADD COLUMN live_revision_id TEXT REFERENCES revisions(id)
		`.execute(db);
		await sql`
			ALTER TABLE ${sql.ref(tableName)}
			ADD COLUMN draft_revision_id TEXT REFERENCES revisions(id)
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${row.slug}_live_revision`)}
			ON ${sql.ref(tableName)} (live_revision_id)
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${row.slug}_draft_revision`)}
			ON ${sql.ref(tableName)} (draft_revision_id)
		`.execute(db);
	}
}
async function down$20(db) {
	const tables = await db.selectFrom("_emdash_collections").select("slug").execute();
	for (const row of tables) {
		const tableName = `ec_${row.slug}`;
		await sql`
			DROP INDEX IF EXISTS ${sql.ref(`idx_${row.slug}_draft_revision`)}
		`.execute(db);
		await sql`
			DROP INDEX IF EXISTS ${sql.ref(`idx_${row.slug}_live_revision`)}
		`.execute(db);
		await sql`
			ALTER TABLE ${sql.ref(tableName)}
			DROP COLUMN draft_revision_id
		`.execute(db);
		await sql`
			ALTER TABLE ${sql.ref(tableName)}
			DROP COLUMN live_revision_id
		`.execute(db);
	}
}

//#endregion
//#region src/database/migrations/015_indexes.ts
var _015_indexes_exports = /* @__PURE__ */ __exportAll({
	down: () => down$19,
	up: () => up$19
});
/**
* Add performance indexes for common query patterns.
*
* Covers:
* 1. Media table: mime_type, filename, created_at
* 2. content_taxonomies: reverse lookup by taxonomy_id
* 3. taxonomies: parent_id FK
* 4. audit_logs: compound (resource_type, resource_id)
* 5. Retroactive author_id + updated_at on existing ec_* content tables
*    (new tables get these from createContentTable() in registry.ts)
*/
async function up$19(db) {
	await db.schema.createIndex("idx_media_mime_type").on("media").column("mime_type").execute();
	await db.schema.createIndex("idx_media_filename").on("media").column("filename").execute();
	await db.schema.createIndex("idx_media_created_at").on("media").column("created_at").execute();
	await db.schema.createIndex("idx_content_taxonomies_term").on("content_taxonomies").column("taxonomy_id").execute();
	await db.schema.createIndex("idx_taxonomies_parent").on("taxonomies").column("parent_id").execute();
	await db.schema.createIndex("idx_audit_resource").on("audit_logs").columns(["resource_type", "resource_id"]).execute();
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_author`)} 
			ON ${sql.ref(table.name)} (author_id)
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_updated`)} 
			ON ${sql.ref(table.name)} (updated_at)
		`.execute(db);
	}
}
async function down$19(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_updated`)}`.execute(db);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_author`)}`.execute(db);
	}
	await db.schema.dropIndex("idx_audit_resource").execute();
	await db.schema.dropIndex("idx_taxonomies_parent").execute();
	await db.schema.dropIndex("idx_content_taxonomies_term").execute();
	await db.schema.dropIndex("idx_media_created_at").execute();
	await db.schema.dropIndex("idx_media_filename").execute();
	await db.schema.dropIndex("idx_media_mime_type").execute();
}

//#endregion
//#region src/database/migrations/016_api_tokens.ts
var _016_api_tokens_exports = /* @__PURE__ */ __exportAll({
	down: () => down$18,
	up: () => up$18
});
/**
* API token tables for programmatic access.
*
* Three tables:
* 1. _emdash_api_tokens — Personal Access Tokens (ec_pat_...)
* 2. _emdash_oauth_tokens — OAuth access/refresh tokens (ec_oat_/ec_ort_...)
* 3. _emdash_device_codes — OAuth Device Flow state (RFC 8628)
*/
async function up$18(db) {
	await db.schema.createTable("_emdash_api_tokens").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).addColumn("token_hash", "text", (col) => col.notNull().unique()).addColumn("prefix", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("expires_at", "text").addColumn("last_used_at", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("api_tokens_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createIndex("idx_api_tokens_token_hash").on("_emdash_api_tokens").column("token_hash").execute();
	await db.schema.createIndex("idx_api_tokens_user_id").on("_emdash_api_tokens").column("user_id").execute();
	await db.schema.createTable("_emdash_oauth_tokens").addColumn("token_hash", "text", (col) => col.primaryKey()).addColumn("token_type", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("client_type", "text", (col) => col.notNull().defaultTo("cli")).addColumn("expires_at", "text", (col) => col.notNull()).addColumn("refresh_token_hash", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("oauth_tokens_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createIndex("idx_oauth_tokens_user_id").on("_emdash_oauth_tokens").column("user_id").execute();
	await db.schema.createIndex("idx_oauth_tokens_expires").on("_emdash_oauth_tokens").column("expires_at").execute();
	await db.schema.createTable("_emdash_device_codes").addColumn("device_code", "text", (col) => col.primaryKey()).addColumn("user_code", "text", (col) => col.notNull().unique()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("user_id", "text").addColumn("status", "text", (col) => col.notNull().defaultTo("pending")).addColumn("expires_at", "text", (col) => col.notNull()).addColumn("interval", "integer", (col) => col.notNull().defaultTo(5)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
}
async function down$18(db) {
	await db.schema.dropTable("_emdash_device_codes").execute();
	await db.schema.dropTable("_emdash_oauth_tokens").execute();
	await db.schema.dropTable("_emdash_api_tokens").execute();
}

//#endregion
//#region src/database/migrations/017_authorization_codes.ts
var _017_authorization_codes_exports = /* @__PURE__ */ __exportAll({
	down: () => down$17,
	up: () => up$17
});
/**
* Authorization codes for OAuth 2.1 Authorization Code + PKCE flow.
*
* Used by MCP clients (Claude Desktop, VS Code, etc.) to authenticate
* via the standard OAuth authorization code grant.
*
* Also adds client_id tracking to oauth_tokens for per-client revocation.
*/
async function up$17(db) {
	await db.schema.createTable("_emdash_authorization_codes").addColumn("code_hash", "text", (col) => col.primaryKey()).addColumn("client_id", "text", (col) => col.notNull()).addColumn("redirect_uri", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("code_challenge", "text", (col) => col.notNull()).addColumn("code_challenge_method", "text", (col) => col.notNull().defaultTo("S256")).addColumn("resource", "text").addColumn("expires_at", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("auth_codes_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
	await db.schema.createIndex("idx_auth_codes_expires").on("_emdash_authorization_codes").column("expires_at").execute();
	await sql`ALTER TABLE _emdash_oauth_tokens ADD COLUMN client_id TEXT`.execute(db);
}
async function down$17(db) {
	await db.schema.dropTable("_emdash_authorization_codes").execute();
}

//#endregion
//#region src/database/migrations/018_seo.ts
var _018_seo_exports = /* @__PURE__ */ __exportAll({
	down: () => down$16,
	up: () => up$16
});
/**
* Migration: SEO support
*
* Creates:
* - `_emdash_seo` table: per-content SEO metadata (separate from content tables)
* - `has_seo` column on `_emdash_collections`: opt-in flag per collection
*
* SEO is not a universal concern — only collections representing web pages
* need it. The `has_seo` flag controls whether the admin shows SEO fields
* and whether the collection's content appears in sitemaps.
*/
async function up$16(db) {
	await db.schema.createTable("_emdash_seo").addColumn("collection", "text", (col) => col.notNull()).addColumn("content_id", "text", (col) => col.notNull()).addColumn("seo_title", "text").addColumn("seo_description", "text").addColumn("seo_image", "text").addColumn("seo_canonical", "text").addColumn("seo_no_index", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("_emdash_seo_pk", ["collection", "content_id"]).execute();
	await sql`
		CREATE INDEX idx_emdash_seo_collection
		ON _emdash_seo (collection)
	`.execute(db);
	await sql`
		ALTER TABLE _emdash_collections
		ADD COLUMN has_seo INTEGER NOT NULL DEFAULT 0
	`.execute(db);
}
async function down$16(db) {
	await sql`DROP TABLE IF EXISTS _emdash_seo`.execute(db);
	await sql`
		ALTER TABLE _emdash_collections
		DROP COLUMN has_seo
	`.execute(db);
}

//#endregion
//#region src/database/migrations/019_i18n.ts
var _019_i18n_exports = /* @__PURE__ */ __exportAll({
	down: () => down$15,
	up: () => up$15
});
/**
* Quote an identifier for use in raw SQL. Escapes embedded double-quotes
* per SQL standard (double them). The name should first pass
* validateIdentifier() or validateTableName() for defense-in-depth.
*/
const DOUBLE_QUOTE_RE = /"/g;
function quoteIdent(name) {
	return `"${name.replace(DOUBLE_QUOTE_RE, "\"\"")}"`;
}
/** Suffix added to tmp tables during i18n migration rebuild. */
const I18N_TMP_SUFFIX = /_i18n_tmp$/;
/** Table names from sqlite_master are ec_{slug} — validate the pattern. */
const TABLE_NAME_PATTERN = /^ec_[a-z][a-z0-9_]*$/;
function validateTableName(name) {
	if (!TABLE_NAME_PATTERN.test(name)) throw new Error(`Invalid content table name: "${name}"`);
}
/** SQLite column types produced by EmDash's schema registry. */
const ALLOWED_COLUMN_TYPES = new Set([
	"TEXT",
	"INTEGER",
	"REAL",
	"BLOB",
	"JSON",
	"NUMERIC",
	""
]);
function validateColumnType(type, colName) {
	if (!ALLOWED_COLUMN_TYPES.has(type.toUpperCase())) throw new Error(`Unexpected column type "${type}" for column "${colName}"`);
}
/**
* Validate that a default value expression from PRAGMA table_info is safe
* to interpolate into DDL. Allows: string literals, numeric literals,
* NULL, and known function calls like datetime('now').
*
* Note: PRAGMA table_info strips the outer parens from expression defaults,
* so `DEFAULT (datetime('now'))` is reported as `datetime('now')`.
* We accept both forms and re-wrap in parens via normalizeDdlDefault().
*/
const SAFE_DEFAULT_PATTERN = /^(?:'[^']*'|NULL|-?\d+(?:\.\d+)?|\(?datetime\('now'\)\)?|\(?json\('[^']*'\)\)?|0|1)$/i;
function validateDefaultValue(value, colName) {
	if (!SAFE_DEFAULT_PATTERN.test(value)) throw new Error(`Unexpected default value "${value}" for column "${colName}"`);
}
/**
* Normalize a PRAGMA table_info default value for use in DDL.
* Function-call defaults (e.g. `datetime('now')`) must be wrapped in parens
* to form valid expression defaults: `DEFAULT (datetime('now'))`.
* PRAGMA strips the outer parens, so we re-add them here.
*/
const FUNCTION_DEFAULT_PATTERN = /^(?:datetime|json)\(/i;
function normalizeDdlDefault(value) {
	if (value.startsWith("(")) return value;
	if (FUNCTION_DEFAULT_PATTERN.test(value)) return `(${value})`;
	return value;
}
/**
* Validate that a CREATE INDEX statement from sqlite_master is safe to replay.
* Must start with CREATE [UNIQUE] INDEX and not contain suspicious patterns.
*/
const CREATE_INDEX_PATTERN = /^CREATE\s+(UNIQUE\s+)?INDEX\s+/i;
function validateCreateIndexSql(sqlStr, idxName) {
	if (!CREATE_INDEX_PATTERN.test(sqlStr)) throw new Error(`Unexpected index SQL for "${idxName}": does not match CREATE INDEX pattern`);
	if (sqlStr.includes(";")) throw new Error(`Unexpected index SQL for "${idxName}": contains semicolon`);
}
/**
* PostgreSQL path: ALTER TABLE supports ADD COLUMN and DROP CONSTRAINT directly.
* No table rebuild needed.
*/
async function upPostgres(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const t of tableNames) {
		validateTableName(t);
		if ((await sql`
			SELECT EXISTS(
				SELECT 1 FROM information_schema.columns
				WHERE table_schema = 'public' AND table_name = ${t} AND column_name = 'locale'
			) as exists
		`.execute(db)).rows[0]?.exists === true) continue;
		await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN locale TEXT NOT NULL DEFAULT 'en'`.execute(db);
		await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN translation_group TEXT`.execute(db);
		const constraints = await sql`
			SELECT conname FROM pg_constraint
			WHERE conrelid = ${t}::regclass
			AND contype = 'u'
			AND array_length(conkey, 1) = 1
			AND conkey[1] = (
				SELECT attnum FROM pg_attribute
				WHERE attrelid = ${t}::regclass AND attname = 'slug'
			)
		`.execute(db);
		for (const c of constraints.rows) await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT ${sql.ref(c.conname)}`.execute(db);
		await sql`
			ALTER TABLE ${sql.ref(t)}
			ADD CONSTRAINT ${sql.ref(`${t}_slug_locale_unique`)} UNIQUE (slug, locale)
		`.execute(db);
		await sql`UPDATE ${sql.ref(t)} SET translation_group = id`.execute(db);
		await sql`CREATE INDEX ${sql.ref(`idx_${t}_locale`)} ON ${sql.ref(t)} (locale)`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)}
			ON ${sql.ref(t)} (translation_group)
		`.execute(db);
	}
	if ((await sql`
		SELECT EXISTS(
			SELECT 1 FROM information_schema.columns
			WHERE table_schema = 'public' AND table_name = '_emdash_fields' AND column_name = 'translatable'
		) as exists
	`.execute(db)).rows[0]?.exists !== true) await sql`
			ALTER TABLE _emdash_fields
			ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
		`.execute(db);
}
async function up$15(db) {
	if (!isSqlite(db)) return upPostgres(db);
	const orphanedTmps = await listTablesLike(db, "ec_%_i18n_tmp");
	for (const tmpName of orphanedTmps) {
		validateTableName(tmpName.replace(I18N_TMP_SUFFIX, ""));
		await sql`DROP TABLE IF EXISTS ${sql.ref(tmpName)}`.execute(db);
	}
	const tables = { rows: (await listTablesLike(db, "ec_%")).map((name) => ({ name })) };
	for (const table of tables.rows) {
		const t = table.name;
		validateTableName(t);
		const tmp = `${t}_i18n_tmp`;
		{
			const trx = db;
			const columns = (await sql`
				PRAGMA table_info(${sql.ref(t)})
			`.execute(trx)).rows;
			if (columns.some((col) => col.name === "locale")) continue;
			const idxResult = await sql`
				PRAGMA index_list(${sql.ref(t)})
			`.execute(trx);
			const indexDefs = [];
			for (const idx of idxResult.rows) {
				if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
				const idxColResult = await sql`
					PRAGMA index_info(${sql.ref(idx.name)})
				`.execute(trx);
				indexDefs.push({
					name: idx.name,
					unique: idx.unique === 1,
					columns: idxColResult.rows.map((c) => c.name),
					partial: idx.partial
				});
			}
			const partialSqls = /* @__PURE__ */ new Map();
			for (const idx of indexDefs) if (idx.partial) {
				const createResult = await sql`
						SELECT sql FROM sqlite_master 
						WHERE type = 'index' AND name = ${idx.name}
					`.execute(trx);
				if (createResult.rows[0]?.sql) partialSqls.set(idx.name, createResult.rows[0].sql);
			}
			for (const col of columns) validateIdentifier(col.name, "column name");
			const colDefs = [];
			const colNames = [];
			for (const col of columns) {
				validateColumnType(col.type || "TEXT", col.name);
				colNames.push(quoteIdent(col.name));
				let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
				if (col.pk) def += " PRIMARY KEY";
				else if (col.name === "slug") {} else if (col.notnull) def += " NOT NULL";
				if (col.dflt_value !== null) {
					validateDefaultValue(col.dflt_value, col.name);
					def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
				}
				colDefs.push(def);
			}
			colDefs.push("\"locale\" TEXT NOT NULL DEFAULT 'en'");
			colDefs.push("\"translation_group\" TEXT");
			colDefs.push("UNIQUE(\"slug\", \"locale\")");
			const createColsSql = colDefs.join(",\n				");
			const selectColsSql = colNames.join(", ");
			for (const idx of indexDefs) await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
			await sql.raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`).execute(trx);
			await sql.raw(`INSERT INTO ${quoteIdent(tmp)} (${selectColsSql}, "locale", "translation_group")\n\t\t\t SELECT ${selectColsSql}, 'en', "id" FROM ${quoteIdent(t)}`).execute(trx);
			await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
			await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
			for (const idx of indexDefs) {
				if (idx.name === `idx_${t}_slug`) continue;
				if (idx.partial && partialSqls.has(idx.name)) {
					const idxSql = partialSqls.get(idx.name);
					validateCreateIndexSql(idxSql, idx.name);
					await sql.raw(idxSql).execute(trx);
				} else {
					for (const c of idx.columns) validateIdentifier(c, "index column name");
					const cols = idx.columns.map((c) => quoteIdent(c)).join(", ");
					const unique = idx.unique ? "UNIQUE " : "";
					await sql.raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${cols})`).execute(trx);
				}
			}
			await sql`
				CREATE INDEX ${sql.ref(`idx_${t}_slug`)} 
				ON ${sql.ref(t)} (slug)
			`.execute(trx);
			await sql`
				CREATE INDEX ${sql.ref(`idx_${t}_locale`)} 
				ON ${sql.ref(t)} (locale)
			`.execute(trx);
			await sql`
				CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)} 
				ON ${sql.ref(t)} (translation_group)
			`.execute(trx);
		}
	}
	if (!(await sql`
		PRAGMA table_info(_emdash_fields)
	`.execute(db)).rows.some((col) => col.name === "translatable")) await sql`
			ALTER TABLE _emdash_fields 
			ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
		`.execute(db);
}
/**
* PostgreSQL down path: straightforward ALTER TABLE operations.
*/
async function downPostgres(db) {
	await sql`ALTER TABLE _emdash_fields DROP COLUMN translatable`.execute(db);
	const tableNames = await listTablesLike(db, "ec_%");
	for (const t of tableNames) {
		validateTableName(t);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_locale`)}`.execute(db);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_translation_group`)}`.execute(db);
		await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT IF EXISTS ${sql.ref(`${t}_slug_locale_unique`)}`.execute(db);
		await sql`ALTER TABLE ${sql.ref(t)} ADD CONSTRAINT ${sql.ref(`${t}_slug_unique`)} UNIQUE (slug)`.execute(db);
		await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN locale`.execute(db);
		await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN translation_group`.execute(db);
	}
}
async function down$15(db) {
	if (!isSqlite(db)) return downPostgres(db);
	await sql`
		ALTER TABLE _emdash_fields
		DROP COLUMN translatable
	`.execute(db);
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const t = tableName;
		validateTableName(t);
		const tmp = `${t}_i18n_tmp`;
		{
			const trx = db;
			const columns = (await sql`
				PRAGMA table_info(${sql.ref(t)})
			`.execute(trx)).rows;
			const idxResult = await sql`
				PRAGMA index_list(${sql.ref(t)})
			`.execute(trx);
			const indexDefs = [];
			for (const idx of idxResult.rows) {
				if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
				const idxColResult = await sql`
					PRAGMA index_info(${sql.ref(idx.name)})
				`.execute(trx);
				indexDefs.push({
					name: idx.name,
					unique: idx.unique === 1,
					columns: idxColResult.rows.map((c) => c.name),
					partial: idx.partial
				});
			}
			const partialSqls = /* @__PURE__ */ new Map();
			for (const idx of indexDefs) if (idx.partial) {
				const createResult = await sql`
						SELECT sql FROM sqlite_master 
						WHERE type = 'index' AND name = ${idx.name}
					`.execute(trx);
				if (createResult.rows[0]?.sql) partialSqls.set(idx.name, createResult.rows[0].sql);
			}
			for (const col of columns) {
				if (col.name === "locale" || col.name === "translation_group") continue;
				validateIdentifier(col.name, "column name");
			}
			const colDefs = [];
			const colNames = [];
			for (const col of columns) {
				if (col.name === "locale" || col.name === "translation_group") continue;
				validateColumnType(col.type || "TEXT", col.name);
				colNames.push(quoteIdent(col.name));
				let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
				if (col.pk) def += " PRIMARY KEY";
				else if (col.name === "slug") def += " UNIQUE";
				else if (col.notnull) def += " NOT NULL";
				if (col.dflt_value !== null) {
					validateDefaultValue(col.dflt_value, col.name);
					def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
				}
				colDefs.push(def);
			}
			const createColsSql = colDefs.join(",\n				");
			const selectColsSql = colNames.join(", ");
			for (const idx of indexDefs) await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
			await sql.raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`).execute(trx);
			await sql.raw(`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
			 SELECT ${selectColsSql} FROM ${quoteIdent(t)}
			 WHERE "locale" = 'en'`).execute(trx);
			await sql.raw(`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
			 SELECT ${selectColsSql} FROM ${quoteIdent(t)}
			 WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
			 AND "id" IN (
				SELECT "id" FROM ${quoteIdent(t)} AS t2
				WHERE t2."translation_group" IS NOT NULL
				AND t2."locale" = (
					SELECT MIN(t3."locale") FROM ${quoteIdent(t)} AS t3
					WHERE t3."translation_group" = t2."translation_group"
				)
			 )`).execute(trx);
			await sql.raw(`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
			 SELECT ${selectColsSql} FROM ${quoteIdent(t)}
			 WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
			 AND "translation_group" IS NULL`).execute(trx);
			await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
			await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
			for (const idx of indexDefs) {
				if (idx.name === `idx_${t}_locale`) continue;
				if (idx.name === `idx_${t}_translation_group`) continue;
				if (idx.partial && partialSqls.has(idx.name)) {
					const idxSql = partialSqls.get(idx.name);
					validateCreateIndexSql(idxSql, idx.name);
					await sql.raw(idxSql).execute(trx);
				} else {
					const cols = idx.columns.filter((c) => c !== "locale" && c !== "translation_group");
					if (cols.length === 0) continue;
					for (const c of cols) validateIdentifier(c, "index column name");
					const colsSql = cols.map((c) => quoteIdent(c)).join(", ");
					const unique = idx.unique ? "UNIQUE " : "";
					await sql.raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${colsSql})`).execute(trx);
				}
			}
		}
	}
}

//#endregion
//#region src/database/migrations/020_collection_url_pattern.ts
var _020_collection_url_pattern_exports = /* @__PURE__ */ __exportAll({
	down: () => down$14,
	up: () => up$14
});
/**
* Migration: URL pattern for collections
*
* Adds `url_pattern` column to `_emdash_collections` so each collection
* can declare its own URL structure (e.g. "/{slug}" for pages, "/blog/{slug}"
* for posts). Used for menu URL resolution, sitemaps, and path-based lookups.
*/
async function up$14(db) {
	await sql`
		ALTER TABLE _emdash_collections
		ADD COLUMN url_pattern TEXT
	`.execute(db);
}
async function down$14(db) {
	await sql`
		ALTER TABLE _emdash_collections
		DROP COLUMN url_pattern
	`.execute(db);
}

//#endregion
//#region src/database/migrations/021_remove_section_categories.ts
var _021_remove_section_categories_exports = /* @__PURE__ */ __exportAll({
	down: () => down$13,
	up: () => up$13
});
/**
* Migration: Remove section categories
*
* Section categories had a complete backend but no UI to create or manage them.
* Rather than building the missing UI for a feature with very little need at this stage,
* we're removing the feature entirely.
*/
async function up$13(db) {
	await db.schema.dropIndex("idx_sections_category").ifExists().execute();
	await db.schema.alterTable("_emdash_sections").dropColumn("category_id").execute();
	await db.schema.dropTable("_emdash_section_categories").execute();
}
async function down$13(db) {
	await db.schema.createTable("_emdash_section_categories").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("sort_order", "integer", (col) => col.defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
	await db.schema.alterTable("_emdash_sections").addColumn("category_id", "text", (col) => col.references("_emdash_section_categories.id").onDelete("set null")).execute();
	await db.schema.createIndex("idx_sections_category").on("_emdash_sections").columns(["category_id"]).execute();
}

//#endregion
//#region src/database/migrations/022_marketplace_plugin_state.ts
var _022_marketplace_plugin_state_exports = /* @__PURE__ */ __exportAll({
	down: () => down$12,
	up: () => up$12
});
/**
* Migration: Add marketplace fields to _plugin_state
*
* Adds `source` and `marketplace_version` columns to track
* whether a plugin was installed from config or marketplace,
* and which marketplace version is installed.
*/
async function up$12(db) {
	await sql`
		ALTER TABLE _plugin_state
		ADD COLUMN source TEXT NOT NULL DEFAULT 'config'
	`.execute(db);
	await sql`
		ALTER TABLE _plugin_state
		ADD COLUMN marketplace_version TEXT
	`.execute(db);
	await sql`
		CREATE INDEX idx_plugin_state_source
		ON _plugin_state (source)
		WHERE source = 'marketplace'
	`.execute(db);
}
async function down$12(db) {
	await sql`
		DROP INDEX IF EXISTS idx_plugin_state_source
	`.execute(db);
	await sql`
		ALTER TABLE _plugin_state
		DROP COLUMN marketplace_version
	`.execute(db);
	await sql`
		ALTER TABLE _plugin_state
		DROP COLUMN source
	`.execute(db);
}

//#endregion
//#region src/database/migrations/023_plugin_metadata.ts
var _023_plugin_metadata_exports = /* @__PURE__ */ __exportAll({
	down: () => down$11,
	up: () => up$11
});
/**
* Migration: Add display metadata to _plugin_state
*
* Stores display_name and description for marketplace plugins
* so the admin UI can show meaningful info without re-fetching
* from the marketplace on every page load.
*/
async function up$11(db) {
	await sql`
		ALTER TABLE _plugin_state
		ADD COLUMN display_name TEXT
	`.execute(db);
	await sql`
		ALTER TABLE _plugin_state
		ADD COLUMN description TEXT
	`.execute(db);
}
async function down$11(db) {
	await sql`
		ALTER TABLE _plugin_state
		DROP COLUMN description
	`.execute(db);
	await sql`
		ALTER TABLE _plugin_state
		DROP COLUMN display_name
	`.execute(db);
}

//#endregion
//#region src/database/migrations/024_media_placeholders.ts
var _024_media_placeholders_exports = /* @__PURE__ */ __exportAll({
	down: () => down$10,
	up: () => up$10
});
/**
* Migration: Add placeholder columns to media table
*
* Stores blurhash and dominant_color for LQIP (Low Quality Image Placeholder)
* support. Generated at upload time from image pixel data.
*/
async function up$10(db) {
	await sql`
		ALTER TABLE media
		ADD COLUMN blurhash TEXT
	`.execute(db);
	await sql`
		ALTER TABLE media
		ADD COLUMN dominant_color TEXT
	`.execute(db);
}
async function down$10(db) {
	await sql`
		ALTER TABLE media
		DROP COLUMN dominant_color
	`.execute(db);
	await sql`
		ALTER TABLE media
		DROP COLUMN blurhash
	`.execute(db);
}

//#endregion
//#region src/database/migrations/025_oauth_clients.ts
var _025_oauth_clients_exports = /* @__PURE__ */ __exportAll({
	down: () => down$9,
	up: () => up$9
});
/**
* Migration: Create OAuth clients table
*
* Implements the oauth_clients registry so that the authorization endpoint
* can validate client_id and enforce a per-client redirect URI allowlist.
*
* Each client has a set of pre-registered redirect URIs (JSON array).
* The authorize endpoint rejects any redirect_uri not in the client's list.
*/
async function up$9(db) {
	await db.schema.createTable("_emdash_oauth_clients").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).addColumn("redirect_uris", "text", (col) => col.notNull()).addColumn("scopes", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
}
async function down$9(db) {
	await db.schema.dropTable("_emdash_oauth_clients").execute();
}

//#endregion
//#region src/database/migrations/026_cron_tasks.ts
var _026_cron_tasks_exports = /* @__PURE__ */ __exportAll({
	down: () => down$8,
	up: () => up$8
});
/**
* Migration: Create cron tasks table for plugin scheduled tasks.
*
* Each plugin can register cron tasks (recurring or one-shot) which are
* stored here and executed by the platform-specific scheduler.
*
* The `next_run_at` + `status` + `enabled` index drives the "find overdue
* tasks" query used by CronExecutor.tick().
*/
async function up$8(db) {
	await db.schema.createTable("_emdash_cron_tasks").addColumn("id", "text", (col) => col.primaryKey()).addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("task_name", "text", (col) => col.notNull()).addColumn("schedule", "text", (col) => col.notNull()).addColumn("is_oneshot", "integer", (col) => col.notNull().defaultTo(0)).addColumn("data", "text").addColumn("next_run_at", "text", (col) => col.notNull()).addColumn("last_run_at", "text").addColumn("status", "text", (col) => col.notNull().defaultTo("idle")).addColumn("locked_at", "text").addColumn("enabled", "integer", (col) => col.notNull().defaultTo(1)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addUniqueConstraint("uq_cron_tasks_plugin_task", ["plugin_id", "task_name"]).execute();
	await db.schema.createIndex("idx_cron_tasks_due").on("_emdash_cron_tasks").columns([
		"enabled",
		"status",
		"next_run_at"
	]).execute();
	await db.schema.createIndex("idx_cron_tasks_plugin").on("_emdash_cron_tasks").column("plugin_id").execute();
}
async function down$8(db) {
	await db.schema.dropTable("_emdash_cron_tasks").execute();
}

//#endregion
//#region src/database/migrations/027_comments.ts
var _027_comments_exports = /* @__PURE__ */ __exportAll({
	down: () => down$7,
	up: () => up$7
});
async function up$7(db) {
	await db.schema.createTable("_emdash_comments").addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection", "text", (col) => col.notNull()).addColumn("content_id", "text", (col) => col.notNull()).addColumn("parent_id", "text", (col) => col.references("_emdash_comments.id").onDelete("cascade")).addColumn("author_name", "text", (col) => col.notNull()).addColumn("author_email", "text", (col) => col.notNull()).addColumn("author_url", "text").addColumn("author_user_id", "text", (col) => col.references("users.id").onDelete("set null")).addColumn("body", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("pending")).addColumn("ip_hash", "text").addColumn("user_agent", "text").addColumn("moderation_metadata", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createIndex("idx_comments_content").on("_emdash_comments").columns([
		"collection",
		"content_id",
		"status"
	]).execute();
	await db.schema.createIndex("idx_comments_parent").on("_emdash_comments").column("parent_id").execute();
	await db.schema.createIndex("idx_comments_status").on("_emdash_comments").columns(["status", "created_at"]).execute();
	await db.schema.createIndex("idx_comments_author_email").on("_emdash_comments").column("author_email").execute();
	await db.schema.createIndex("idx_comments_author_user").on("_emdash_comments").column("author_user_id").execute();
	await db.schema.alterTable("_emdash_collections").addColumn("comments_enabled", "integer", (col) => col.defaultTo(0)).execute();
	await db.schema.alterTable("_emdash_collections").addColumn("comments_moderation", "text", (col) => col.defaultTo("first_time")).execute();
	await db.schema.alterTable("_emdash_collections").addColumn("comments_closed_after_days", "integer", (col) => col.defaultTo(90)).execute();
	await db.schema.alterTable("_emdash_collections").addColumn("comments_auto_approve_users", "integer", (col) => col.defaultTo(1)).execute();
}
async function down$7(db) {
	await db.schema.dropTable("_emdash_comments").execute();
}

//#endregion
//#region src/database/migrations/028_drop_author_url.ts
var _028_drop_author_url_exports = /* @__PURE__ */ __exportAll({
	down: () => down$6,
	up: () => up$6
});
async function up$6(db) {
	await sql`ALTER TABLE _emdash_comments DROP COLUMN author_url`.execute(db);
}
async function down$6(db) {
	await db.schema.alterTable("_emdash_comments").addColumn("author_url", "text").execute();
}

//#endregion
//#region src/database/migrations/029_redirects.ts
var _029_redirects_exports = /* @__PURE__ */ __exportAll({
	down: () => down$5,
	up: () => up$5
});
async function up$5(db) {
	await db.schema.createTable("_emdash_redirects").addColumn("id", "text", (col) => col.primaryKey()).addColumn("source", "text", (col) => col.notNull()).addColumn("destination", "text", (col) => col.notNull()).addColumn("type", "integer", (col) => col.notNull().defaultTo(301)).addColumn("is_pattern", "integer", (col) => col.notNull().defaultTo(0)).addColumn("enabled", "integer", (col) => col.notNull().defaultTo(1)).addColumn("hits", "integer", (col) => col.notNull().defaultTo(0)).addColumn("last_hit_at", "text").addColumn("group_name", "text").addColumn("auto", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createIndex("idx_redirects_source").on("_emdash_redirects").column("source").execute();
	await db.schema.createIndex("idx_redirects_enabled").on("_emdash_redirects").column("enabled").execute();
	await db.schema.createIndex("idx_redirects_group").on("_emdash_redirects").column("group_name").execute();
	await db.schema.createTable("_emdash_404_log").addColumn("id", "text", (col) => col.primaryKey()).addColumn("path", "text", (col) => col.notNull()).addColumn("referrer", "text").addColumn("user_agent", "text").addColumn("ip", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await db.schema.createIndex("idx_404_log_path").on("_emdash_404_log").column("path").execute();
	await db.schema.createIndex("idx_404_log_created").on("_emdash_404_log").column("created_at").execute();
}
async function down$5(db) {
	await db.schema.dropTable("_emdash_404_log").execute();
	await db.schema.dropTable("_emdash_redirects").execute();
}

//#endregion
//#region src/database/migrations/030_widen_scheduled_index.ts
var _030_widen_scheduled_index_exports = /* @__PURE__ */ __exportAll({
	down: () => down$4,
	up: () => up$4
});
/**
* Migration: Widen scheduled publishing index
*
* The original partial index (013) only covered status='scheduled'.
* Published posts can now have scheduled draft changes, so widen the
* index to cover all rows where scheduled_at IS NOT NULL.
*/
async function up$4(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`
			DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_scheduled`)}
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_scheduled`)}
			ON ${sql.ref(table.name)} (scheduled_at)
			WHERE scheduled_at IS NOT NULL
		`.execute(db);
	}
}
async function down$4(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`
			DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_scheduled`)}
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_scheduled`)}
			ON ${sql.ref(table.name)} (scheduled_at)
			WHERE scheduled_at IS NOT NULL AND status = 'scheduled'
		`.execute(db);
	}
}

//#endregion
//#region src/database/migrations/031_bylines.ts
var _031_bylines_exports = /* @__PURE__ */ __exportAll({
	down: () => down$3,
	up: () => up$3
});
async function up$3(db) {
	await db.schema.createTable("_emdash_bylines").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("display_name", "text", (col) => col.notNull()).addColumn("bio", "text").addColumn("avatar_media_id", "text", (col) => col.references("media.id").onDelete("set null")).addColumn("website_url", "text").addColumn("user_id", "text", (col) => col.references("users.id").onDelete("set null")).addColumn("is_guest", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
	await sql`
		CREATE UNIQUE INDEX ${sql.ref("idx_bylines_user_id_unique")}
		ON ${sql.ref("_emdash_bylines")} (user_id)
		WHERE user_id IS NOT NULL
	`.execute(db);
	await db.schema.createIndex("idx_bylines_slug").on("_emdash_bylines").column("slug").execute();
	await db.schema.createIndex("idx_bylines_display_name").on("_emdash_bylines").column("display_name").execute();
	await db.schema.createTable("_emdash_content_bylines").addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection_slug", "text", (col) => col.notNull()).addColumn("content_id", "text", (col) => col.notNull()).addColumn("byline_id", "text", (col) => col.notNull().references("_emdash_bylines.id").onDelete("cascade")).addColumn("sort_order", "integer", (col) => col.notNull().defaultTo(0)).addColumn("role_label", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addUniqueConstraint("content_bylines_unique", [
		"collection_slug",
		"content_id",
		"byline_id"
	]).execute();
	await db.schema.createIndex("idx_content_bylines_content").on("_emdash_content_bylines").columns([
		"collection_slug",
		"content_id",
		"sort_order"
	]).execute();
	await db.schema.createIndex("idx_content_bylines_byline").on("_emdash_content_bylines").column("byline_id").execute();
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		await sql`
			ALTER TABLE ${sql.ref(tableName)}
			ADD COLUMN primary_byline_id TEXT
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${tableName}_primary_byline`)}
			ON ${sql.ref(tableName)} (primary_byline_id)
		`.execute(db);
	}
}
async function down$3(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		await sql`
			DROP INDEX IF EXISTS ${sql.ref(`idx_${tableName}_primary_byline`)}
		`.execute(db);
		await sql`
			ALTER TABLE ${sql.ref(tableName)}
			DROP COLUMN primary_byline_id
		`.execute(db);
	}
	await db.schema.dropTable("_emdash_content_bylines").execute();
	await db.schema.dropTable("_emdash_bylines").execute();
}

//#endregion
//#region src/database/migrations/032_rate_limits.ts
var _032_rate_limits_exports = /* @__PURE__ */ __exportAll({
	down: () => down$2,
	up: () => up$2
});
/**
* Migration: Rate limits table + device code polling tracking.
*
* 1. Create _emdash_rate_limits for database-backed rate limiting
*    of unauthenticated endpoints (device code, magic link, passkey).
*
* 2. Add last_polled_at column to _emdash_device_codes for
*    RFC 8628 slow_down enforcement.
*/
async function up$2(db) {
	await db.schema.createTable("_emdash_rate_limits").addColumn("key", "text", (col) => col.notNull()).addColumn("window", "text", (col) => col.notNull()).addColumn("count", "integer", (col) => col.notNull().defaultTo(1)).addPrimaryKeyConstraint("pk_rate_limits", ["key", "window"]).execute();
	await db.schema.createIndex("idx_rate_limits_window").on("_emdash_rate_limits").column("window").execute();
	await db.schema.alterTable("_emdash_device_codes").addColumn("last_polled_at", "text").execute();
}
async function down$2(db) {
	await db.schema.dropTable("_emdash_rate_limits").execute();
	await db.schema.alterTable("_emdash_device_codes").dropColumn("last_polled_at").execute();
}

//#endregion
//#region src/database/migrations/033_optimize_content_indexes.ts
var _033_optimize_content_indexes_exports = /* @__PURE__ */ __exportAll({
	down: () => down$1,
	up: () => up$1
});
/**
* Migration: Optimize content table indexes for D1 performance
*
* Addresses GitHub issue #131: Full table scans causing massive D1 row reads.
*
* Changes:
* 1. Replaces single-column indexes with composite indexes on ec_* tables
* 2. Adds partial indexes for _emdash_comments status counting
*
* Impact: Reduces D1 row reads by 90%+ for admin panel operations.
*/
async function up$1(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_status`)}`.execute(db);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_created`)}`.execute(db);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted`)}`.execute(db);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_updated`)}`.execute(db);
		await sql`
			CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_updated_id`)}
			ON ${sql.ref(table.name)} (deleted_at, updated_at DESC, id DESC)
		`.execute(db);
		await sql`
			CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_status`)}
			ON ${sql.ref(table.name)} (deleted_at, status)
		`.execute(db);
		await sql`
			CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_created_id`)}
			ON ${sql.ref(table.name)} (deleted_at, created_at DESC, id DESC)
		`.execute(db);
	}
	await sql`
		CREATE INDEX IF NOT EXISTS idx_comments_pending
		ON _emdash_comments (id)
		WHERE status = 'pending'
	`.execute(db);
	await sql`
		CREATE INDEX IF NOT EXISTS idx_comments_approved
		ON _emdash_comments (id)
		WHERE status = 'approved'
	`.execute(db);
	await sql`
		CREATE INDEX IF NOT EXISTS idx_comments_spam
		ON _emdash_comments (id)
		WHERE status = 'spam'
	`.execute(db);
	await sql`
		CREATE INDEX IF NOT EXISTS idx_comments_trash
		ON _emdash_comments (id)
		WHERE status = 'trash'
	`.execute(db);
}
async function down$1(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_updated_id`)}`.execute(db);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_status`)}`.execute(db);
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_created_id`)}`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_status`)}
			ON ${sql.ref(table.name)} (status)
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_created`)}
			ON ${sql.ref(table.name)} (created_at)
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_deleted`)}
			ON ${sql.ref(table.name)} (deleted_at)
		`.execute(db);
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_updated`)}
			ON ${sql.ref(table.name)} (updated_at)
		`.execute(db);
	}
	await sql`DROP INDEX IF EXISTS idx_comments_pending`.execute(db);
	await sql`DROP INDEX IF EXISTS idx_comments_approved`.execute(db);
	await sql`DROP INDEX IF EXISTS idx_comments_spam`.execute(db);
	await sql`DROP INDEX IF EXISTS idx_comments_trash`.execute(db);
}

//#endregion
//#region src/database/migrations/034_published_at_index.ts
var _034_published_at_index_exports = /* @__PURE__ */ __exportAll({
	down: () => down,
	up: () => up
});
async function up(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`
			CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_published_id`)}
			ON ${sql.ref(table.name)} (deleted_at, published_at DESC, id DESC)
		`.execute(db);
	}
}
async function down(db) {
	const tableNames = await listTablesLike(db, "ec_%");
	for (const tableName of tableNames) {
		const table = { name: tableName };
		await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_published_id`)}`.execute(db);
	}
}

//#endregion
//#region src/database/migrations/runner.ts
const MIGRATIONS = Object.freeze({
	"001_initial": _001_initial_exports,
	"002_media_status": _002_media_status_exports,
	"003_schema_registry": _003_schema_registry_exports,
	"004_plugins": _004_plugins_exports,
	"005_menus": _005_menus_exports,
	"006_taxonomy_defs": _006_taxonomy_defs_exports,
	"007_widgets": _007_widgets_exports,
	"008_auth": _008_auth_exports,
	"009_user_disabled": _009_user_disabled_exports,
	"011_sections": _011_sections_exports,
	"012_search": _012_search_exports,
	"013_scheduled_publishing": _013_scheduled_publishing_exports,
	"014_draft_revisions": _014_draft_revisions_exports,
	"015_indexes": _015_indexes_exports,
	"016_api_tokens": _016_api_tokens_exports,
	"017_authorization_codes": _017_authorization_codes_exports,
	"018_seo": _018_seo_exports,
	"019_i18n": _019_i18n_exports,
	"020_collection_url_pattern": _020_collection_url_pattern_exports,
	"021_remove_section_categories": _021_remove_section_categories_exports,
	"022_marketplace_plugin_state": _022_marketplace_plugin_state_exports,
	"023_plugin_metadata": _023_plugin_metadata_exports,
	"024_media_placeholders": _024_media_placeholders_exports,
	"025_oauth_clients": _025_oauth_clients_exports,
	"026_cron_tasks": _026_cron_tasks_exports,
	"027_comments": _027_comments_exports,
	"028_drop_author_url": _028_drop_author_url_exports,
	"029_redirects": _029_redirects_exports,
	"030_widen_scheduled_index": _030_widen_scheduled_index_exports,
	"031_bylines": _031_bylines_exports,
	"032_rate_limits": _032_rate_limits_exports,
	"033_optimize_content_indexes": _033_optimize_content_indexes_exports,
	"034_published_at_index": _034_published_at_index_exports
});
/** Total number of registered migrations. Exported for use in tests. */
const MIGRATION_COUNT = Object.keys(MIGRATIONS).length;
/**
* Migration provider that uses statically imported migrations.
* This approach works well with bundlers and avoids filesystem access.
*/
var StaticMigrationProvider = class {
	async getMigrations() {
		return MIGRATIONS;
	}
};
/** Custom migration table name */
const MIGRATION_TABLE = "_emdash_migrations";
const MIGRATION_LOCK_TABLE = "_emdash_migrations_lock";
/**
* Get migration status
*/
async function getMigrationStatus(db) {
	const migrations = await new Migrator({
		db,
		provider: new StaticMigrationProvider(),
		migrationTableName: MIGRATION_TABLE,
		migrationLockTableName: MIGRATION_LOCK_TABLE
	}).getMigrations();
	const applied = [];
	const pending = [];
	for (const migration of migrations) if (migration.executedAt) applied.push(migration.name);
	else pending.push(migration.name);
	return {
		applied,
		pending
	};
}
/**
* Run all pending migrations.
*
* Includes a fast-path: if the migration table already exists and contains
* exactly MIGRATION_COUNT rows, all migrations have been applied and we can
* skip the Kysely Migrator entirely. This avoids the expensive
* `pragma_table_info` introspection that Kysely runs for every table in the
* database (twice!) just to check if the migration tables exist.
* On D1 with ~57 tables, that's ~116 queries saved per init.
*/
async function runMigrations(db) {
	try {
		if ((await sql`
			SELECT COUNT(*) as count FROM ${sql.ref(MIGRATION_TABLE)}
		`.execute(db)).rows[0]?.count === MIGRATION_COUNT) return { applied: [] };
	} catch {}
	const { error, results } = await new Migrator({
		db,
		provider: new StaticMigrationProvider(),
		migrationTableName: MIGRATION_TABLE,
		migrationLockTableName: MIGRATION_LOCK_TABLE
	}).migrateToLatest();
	const applied = results?.filter((r) => r.status === "Success").map((r) => r.migrationName) ?? [];
	if (error) {
		let msg = error instanceof Error ? error.message : JSON.stringify(error);
		if (!msg && error instanceof Error && error.cause) msg = error.cause instanceof Error ? error.cause.message : JSON.stringify(error.cause);
		const failedMigration = results?.find((r) => r.status === "Error");
		if (failedMigration) msg = `${msg || "unknown error"} (migration: ${failedMigration.migrationName})`;
		throw new Error(`Migration failed: ${msg}`);
	}
	return { applied };
}
/**
* Rollback the last migration
*/
async function rollbackMigration(db) {
	const { error, results } = await new Migrator({
		db,
		provider: new StaticMigrationProvider(),
		migrationTableName: MIGRATION_TABLE,
		migrationLockTableName: MIGRATION_LOCK_TABLE
	}).migrateDown();
	const rolledBack = results?.[0]?.status === "Success" ? results[0].migrationName : null;
	if (error) {
		const msg = error instanceof Error ? error.message : JSON.stringify(error);
		throw new Error(`Rollback failed: ${msg}`);
	}
	return { rolledBack };
}

//#endregion
export { rollbackMigration as n, runMigrations as r, getMigrationStatus as t };
//# sourceMappingURL=runner-Cd-_WyDo.mjs.map