{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-drizzle",
  "title": "Data Table Drizzle ORM Helpers",
  "description": "Server-side filtering, faceted search, cursor pagination, and sorting helpers for Drizzle ORM.",
  "dependencies": [
    "drizzle-orm",
    "date-fns"
  ],
  "registryDependencies": [
    "https://data-table.openstatus.dev/r/data-table.json",
    "https://data-table.openstatus.dev/r/data-table-schema.json"
  ],
  "files": [
    {
      "path": "src/lib/drizzle/index.ts",
      "content": "export { buildWhereConditions } from \"./filters\";\nexport { computeFacets } from \"./facets\";\nexport { buildOrderBy } from \"./sorting\";\nexport { buildCursorPagination } from \"./pagination\";\nexport { createDrizzleHandler } from \"./handler\";\nexport type { DrizzleHandlerConfig, DrizzleHandlerResult } from \"./handler\";\nexport type {\n  ColumnMapping,\n  DrizzleDB,\n  SortDescriptor,\n  CursorPaginationParams,\n} from \"./types\";\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/drizzle/types.ts",
      "content": "import type { Column } from \"drizzle-orm\";\nimport type { PgDatabase } from \"drizzle-orm/pg-core\";\n\n/**\n * Maps tableSchema field keys to Drizzle table columns.\n * This is the bridge between your table UI schema and your database.\n *\n * @example\n * const columnMapping = {\n *   level: logs.level,\n *   date: logs.date,\n *   latency: logs.latency,\n *   \"timing.dns\": logs.timingDns,\n * } satisfies ColumnMapping;\n */\nexport type ColumnMapping = Record<string, Column>;\n\nexport type DrizzleDB = PgDatabase<any, any, any>;\n\n/** Sort descriptor matching the URL state shape. */\nexport type SortDescriptor = { id: string; desc: boolean } | null;\n\n/** Cursor-based pagination params. */\nexport type CursorPaginationParams = {\n  cursor: Date | number | null;\n  direction: \"prev\" | \"next\";\n  size: number;\n  cursorColumn: Column;\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/drizzle/handler.ts",
      "content": "import type { FacetMetadataSchema } from \"@/lib/data-table/types\";\nimport type { TableSchemaDefinition } from \"@/lib/table-schema\";\nimport { and, count, sql, type SQL } from \"drizzle-orm\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\nimport { computeFacets } from \"./facets\";\nimport { buildWhereConditions } from \"./filters\";\nimport { buildCursorPagination } from \"./pagination\";\nimport { buildOrderBy } from \"./sorting\";\nimport type { ColumnMapping, DrizzleDB, SortDescriptor } from \"./types\";\n\n/**\n * Derive slider, facet, and date keys from a tableSchema definition.\n * Reads `builder._config.filter.type` so no manual key lists are needed.\n */\nfunction deriveKeys(schema: TableSchemaDefinition) {\n  const sliderKeys: string[] = [];\n  const facetKeys: string[] = [];\n  const dateKeys: string[] = [];\n\n  for (const [key, builder] of Object.entries(schema)) {\n    const filter = builder._config.filter;\n    if (!filter) continue;\n    if (filter.type === \"slider\") {\n      sliderKeys.push(key);\n      facetKeys.push(key);\n    }\n    if (filter.type === \"checkbox\") facetKeys.push(key);\n    if (filter.type === \"input\") facetKeys.push(key);\n    if (filter.type === \"timerange\") dateKeys.push(key);\n  }\n\n  return { sliderKeys, facetKeys, dateKeys };\n}\n\ntype DrizzleHandlerKeys = {\n  sliderKeys: string[];\n  facetKeys: string[];\n  dateKeys: string[];\n};\n\ntype DrizzleHandlerConfigBase = {\n  db: DrizzleDB;\n  table: PgTable;\n  columnMapping: ColumnMapping;\n  cursorColumn: string;\n  defaultSize?: number;\n};\n\n/**\n * Config with explicit key lists — use when `tableSchema` is in a\n * `\"use client\"` file and cannot be imported on the server.\n */\nexport type DrizzleHandlerConfigWithKeys = DrizzleHandlerConfigBase &\n  DrizzleHandlerKeys;\n\n/**\n * Config with `tableSchema.definition` — slider/facet/date keys are\n * derived automatically from `builder._config.filter.type`.\n */\nexport type DrizzleHandlerConfigWithSchema = DrizzleHandlerConfigBase & {\n  schema: TableSchemaDefinition;\n};\n\nexport type DrizzleHandlerConfig =\n  | DrizzleHandlerConfigWithKeys\n  | DrizzleHandlerConfigWithSchema;\n\nexport type DrizzleHandlerResult<TRow = Record<string, unknown>> = {\n  data: TRow[];\n  facets: Record<string, FacetMetadataSchema>;\n  totalRowCount: number;\n  filterRowCount: number;\n  nextCursor: number | null;\n  prevCursor: number | null;\n  /** The combined WHERE conditions (all three passes). Useful for chart queries. */\n  allConditions: SQL[];\n};\n\nfunction resolveKeys(config: DrizzleHandlerConfig): DrizzleHandlerKeys {\n  if (\"schema\" in config) {\n    return deriveKeys(config.schema);\n  }\n  return {\n    sliderKeys: config.sliderKeys,\n    facetKeys: config.facetKeys,\n    dateKeys: config.dateKeys,\n  };\n}\n\n/**\n * Create a high-level query handler that encapsulates the three-pass filtering\n * strategy, faceted search, counts, and cursor pagination.\n *\n * Chart data and percentiles are intentionally excluded — handle them in user-land.\n *\n * @example\n * ```ts\n * // Option 1: Derive keys from tableSchema (when importable)\n * const handler = createDrizzleHandler({\n *   db,\n *   table: logs,\n *   schema: tableSchema.definition,\n *   columnMapping,\n *   cursorColumn: \"date\",\n * });\n *\n * // Option 2: Explicit keys (when tableSchema is \"use client\")\n * const handler = createDrizzleHandler({\n *   db,\n *   table: logs,\n *   columnMapping,\n *   cursorColumn: \"date\",\n *   sliderKeys: [\"latency\", \"timing.dns\", ...],\n *   facetKeys: [\"level\", \"method\", \"latency\", ...],\n *   dateKeys: [\"date\"],\n * });\n *\n * const result = await handler.execute(search);\n * ```\n */\nexport function createDrizzleHandler(config: DrizzleHandlerConfig) {\n  const { db, table, columnMapping, cursorColumn, defaultSize = 40 } = config;\n  const { sliderKeys, facetKeys, dateKeys } = resolveKeys(config);\n\n  const cursorCol = columnMapping[cursorColumn];\n  if (!cursorCol) {\n    throw new Error(\n      `cursorColumn \"${cursorColumn}\" not found in columnMapping`,\n    );\n  }\n\n  return {\n    /** Derived keys (exposed for advanced use cases) */\n    sliderKeys,\n    facetKeys,\n    dateKeys,\n\n    async execute(\n      search: Record<string, unknown>,\n    ): Promise<DrizzleHandlerResult> {\n      const size = typeof search.size === \"number\" ? search.size : defaultSize;\n      const sort = (search.sort as SortDescriptor) ?? null;\n      const cursor = (search.cursor as Date | number | null) ?? null;\n      const direction = (search.direction as \"prev\" | \"next\") ?? \"next\";\n\n      // --- Three-pass filtering strategy ---\n\n      // Pass 1: Date range conditions only\n      const dateFilters = Object.fromEntries(\n        Object.entries(search).filter(([key]) => dateKeys.includes(key)),\n      );\n      const dateConditions = buildWhereConditions(columnMapping, dateFilters);\n\n      // Pass 2: Date + non-slider filters (for slider facet bounds)\n      const nonSliderFilters = Object.fromEntries(\n        Object.entries(search).filter(\n          ([key]) => !sliderKeys.includes(key) && !dateKeys.includes(key),\n        ),\n      );\n      const nonSliderConditions = buildWhereConditions(\n        columnMapping,\n        nonSliderFilters,\n      );\n      const pass2Conditions = [...dateConditions, ...nonSliderConditions];\n\n      // Pass 3: All conditions including sliders\n      const sliderFilters = Object.fromEntries(\n        Object.entries(search).filter(([key]) => sliderKeys.includes(key)),\n      );\n      const sliderConditions = buildWhereConditions(\n        columnMapping,\n        sliderFilters,\n      );\n      const allConditions = [...pass2Conditions, ...sliderConditions];\n\n      // --- Facets (parallel) ---\n      const [sliderFacets, otherFacets] = await Promise.all([\n        computeFacets(db, table, columnMapping, pass2Conditions, sliderKeys, {\n          sliderKeys,\n        }),\n        computeFacets(\n          db,\n          table,\n          columnMapping,\n          allConditions,\n          facetKeys.filter((k) => !sliderKeys.includes(k)),\n        ),\n      ]);\n\n      const facets = { ...sliderFacets, ...otherFacets };\n\n      // --- Counts (parallel) ---\n      const allWhere =\n        allConditions.length > 0 ? and(...allConditions) : undefined;\n\n      const [totalResult, filterResult] = await Promise.all([\n        db.select({ total: count() }).from(table),\n        db.select({ total: count() }).from(table).where(allWhere),\n      ]);\n\n      const totalRowCount = totalResult[0]?.total ?? 0;\n      const filterRowCount = filterResult[0]?.total ?? 0;\n\n      // --- Sort + Cursor Pagination ---\n      const orderBy = buildOrderBy(columnMapping, sort);\n\n      const {\n        cursorCondition,\n        orderBy: cursorOrderBy,\n        needsReverse,\n      } = buildCursorPagination({\n        cursor,\n        direction,\n        size,\n        cursorColumn: cursorCol,\n      });\n\n      const dataConditions = cursorCondition\n        ? [...allConditions, cursorCondition]\n        : allConditions;\n\n      const dataWhere =\n        dataConditions.length > 0 ? and(...dataConditions) : undefined;\n\n      const orderClauses = orderBy\n        ? sql`${cursorOrderBy}, ${orderBy}`\n        : cursorOrderBy;\n\n      const rows = await db\n        .select()\n        .from(table)\n        .where(dataWhere)\n        .orderBy(orderClauses)\n        .limit(size);\n\n      if (needsReverse) {\n        rows.reverse();\n      }\n\n      // --- Cursors ---\n      const lastRow = rows[rows.length - 1];\n      const firstRow = rows[0];\n\n      const getCursorValue = (row: Record<string, unknown>): number | null => {\n        if (!row) return null;\n        const val = row[cursorCol.name];\n        if (val instanceof Date) return val.getTime();\n        if (typeof val === \"number\") return val;\n        return null;\n      };\n\n      const nextCursor = lastRow ? getCursorValue(lastRow) : null;\n      const prevCursor = firstRow\n        ? getCursorValue(firstRow)\n        : new Date().getTime();\n\n      return {\n        data: rows,\n        facets,\n        totalRowCount,\n        filterRowCount,\n        nextCursor,\n        prevCursor,\n        allConditions,\n      };\n    },\n  };\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/drizzle/facets.ts",
      "content": "import type { FacetMetadataSchema } from \"@/lib/data-table/types\";\nimport { and, count, max, min, sql, type SQL } from \"drizzle-orm\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\nimport type { ColumnMapping, DrizzleDB } from \"./types\";\n\n/**\n * Compute faceted counts for filter fields via SQL.\n *\n * For each key:\n * - Slider fields: SELECT MIN(col), MAX(col), COUNT(*)\n * - Array columns: unnest + GROUP BY for unique values\n * - Standard columns: GROUP BY for unique values with counts\n *\n * All facet queries run in parallel via Promise.all.\n */\nexport async function computeFacets(\n  db: DrizzleDB,\n  table: PgTable,\n  mapping: ColumnMapping,\n  baseConditions: SQL[],\n  facetKeys: string[],\n  options?: {\n    /** Keys that should return min/max instead of grouped values */\n    sliderKeys?: string[];\n  },\n): Promise<Record<string, FacetMetadataSchema>> {\n  const whereCondition =\n    baseConditions.length > 0 ? and(...baseConditions) : undefined;\n  const sliderKeys = options?.sliderKeys ?? [];\n\n  const queries = facetKeys.map(async (key) => {\n    const column = mapping[key];\n    if (!column) return [key, null] as const;\n\n    if (sliderKeys.includes(key)) {\n      const result = await db\n        .select({\n          min: min(column),\n          max: max(column),\n          total: count(),\n        })\n        .from(table)\n        .where(whereCondition);\n\n      const row = result[0];\n\n      return [\n        key,\n        {\n          rows: [],\n          total: Number(row?.total ?? 0),\n          min: row?.min != null ? Number(row.min) : undefined,\n          max: row?.max != null ? Number(row.max) : undefined,\n        } satisfies FacetMetadataSchema,\n      ] as const;\n    }\n\n    // Array columns: unnest + GROUP BY\n    if (column.dataType === \"array\") {\n      const raw = await db.execute(\n        sql`SELECT val as value, COUNT(*)::int as total\n            FROM (\n              SELECT unnest(${column}) as val\n              FROM ${table}\n              ${whereCondition ? sql`WHERE ${whereCondition}` : sql``}\n            ) sub\n            GROUP BY val\n            ORDER BY total DESC`,\n      );\n      const result: { value: string; total: number }[] = Array.isArray(raw)\n        ? raw\n        : (raw as { rows: { value: string; total: number }[] }).rows;\n\n      const total = result.reduce((sum, r) => sum + Number(r.total), 0);\n\n      return [\n        key,\n        {\n          rows: result.map((r) => ({\n            value: r.value,\n            total: Number(r.total),\n          })),\n          total,\n        } satisfies FacetMetadataSchema,\n      ] as const;\n    }\n\n    // Standard column: GROUP BY\n    const raw = await db.execute(\n      sql`SELECT ${column} as value, COUNT(*)::int as total\n          FROM ${table}\n          ${whereCondition ? sql`WHERE ${whereCondition}` : sql``}\n          GROUP BY ${column}\n          ORDER BY total DESC`,\n    );\n    const result: { value: string | number | boolean; total: number }[] =\n      Array.isArray(raw)\n        ? raw\n        : (\n            raw as {\n              rows: { value: string | number | boolean; total: number }[];\n            }\n          ).rows;\n\n    const total = result.reduce((sum, r) => sum + Number(r.total), 0);\n\n    let minVal: number | undefined;\n    let maxVal: number | undefined;\n    if (result.length > 0 && typeof result[0].value === \"number\") {\n      minVal = Math.min(...result.map((r) => Number(r.value)));\n      maxVal = Math.max(...result.map((r) => Number(r.value)));\n    }\n\n    return [\n      key,\n      {\n        rows: result.map((r) => ({\n          value: r.value,\n          total: Number(r.total),\n        })),\n        total,\n        min: minVal,\n        max: maxVal,\n      } satisfies FacetMetadataSchema,\n    ] as const;\n  });\n\n  const results = await Promise.all(queries);\n  return Object.fromEntries(results.filter(([, v]) => v !== null));\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/drizzle/filters.ts",
      "content": "import { endOfDay, startOfDay } from \"date-fns\";\nimport {\n  and,\n  between,\n  eq,\n  gte,\n  ilike,\n  inArray,\n  lte,\n  sql,\n  type SQL,\n} from \"drizzle-orm\";\nimport type { ColumnMapping } from \"./types\";\n\n/**\n * Build WHERE conditions from filter state and a column mapping.\n *\n * Dispatches by filter value shape:\n * - string → ilike (text search)\n * - number → eq\n * - Date[] → date range (gte/lte or same-day)\n * - number[] → between (slider range) or inArray (checkbox with numbers)\n * - string[] → inArray (checkbox with strings)\n *\n * @param mapping - Maps filter keys to Drizzle columns\n * @param filters - Current filter values from parsed search params\n * @param options - { exclude, only } to skip/limit keys (for three-pass strategy)\n */\nexport function buildWhereConditions(\n  mapping: ColumnMapping,\n  filters: Record<string, unknown>,\n  options?: { exclude?: string[]; only?: string[] },\n): SQL[] {\n  const conditions: SQL[] = [];\n\n  for (const [key, value] of Object.entries(filters)) {\n    if (value === null || value === undefined) continue;\n    if (Array.isArray(value) && value.length === 0) continue;\n    if (options?.exclude?.includes(key)) continue;\n    if (options?.only && !options.only.includes(key)) continue;\n\n    const column = mapping[key];\n    if (!column) continue;\n\n    // String → text search (ilike)\n    if (typeof value === \"string\") {\n      conditions.push(ilike(column, `%${value}%`));\n      continue;\n    }\n\n    // Number → exact match\n    if (typeof value === \"number\") {\n      conditions.push(eq(column, value));\n      continue;\n    }\n\n    // Boolean → exact match\n    if (typeof value === \"boolean\") {\n      conditions.push(eq(column, value));\n      continue;\n    }\n\n    // Array handling\n    if (Array.isArray(value)) {\n      // Date array → date range\n      if (value[0] instanceof Date) {\n        const dates = value as Date[];\n        if (dates.length === 1) {\n          conditions.push(\n            and(\n              gte(column, startOfDay(dates[0])),\n              lte(column, endOfDay(dates[0])),\n            )!,\n          );\n        } else if (dates.length === 2) {\n          conditions.push(and(gte(column, dates[0]), lte(column, dates[1]))!);\n        }\n        continue;\n      }\n\n      // Number array → slider range or checkbox\n      if (typeof value[0] === \"number\") {\n        const nums = value as number[];\n        if (nums.length === 1) {\n          conditions.push(eq(column, nums[0]));\n        } else if (nums.length === 2) {\n          conditions.push(between(column, nums[0], nums[1]));\n        } else {\n          conditions.push(inArray(column, nums));\n        }\n        continue;\n      }\n\n      // String array → checkbox (inArray) or pg array overlap\n      if (typeof value[0] === \"string\") {\n        const strs = value as string[];\n        if (column.dataType === \"array\") {\n          conditions.push(\n            sql`${column} && ARRAY[${sql.join(\n              strs.map((s) => sql`${s}`),\n              sql`, `,\n            )}]::text[]`,\n          );\n        } else {\n          conditions.push(inArray(column, strs));\n        }\n        continue;\n      }\n    }\n  }\n\n  return conditions;\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/drizzle/pagination.ts",
      "content": "import { asc, desc, gt, lt, type SQL } from \"drizzle-orm\";\nimport type { CursorPaginationParams } from \"./types\";\n\n/**\n * Build cursor-based pagination conditions and ordering.\n *\n * - direction \"next\": fetch rows BEFORE cursor (older) → ORDER BY cursorCol DESC\n * - direction \"prev\": fetch rows AFTER cursor (newer) → ORDER BY cursorCol ASC\n */\nexport function buildCursorPagination(params: CursorPaginationParams): {\n  cursorCondition: SQL | undefined;\n  orderBy: SQL;\n  needsReverse: boolean;\n} {\n  const { cursor, direction, cursorColumn } = params;\n\n  const cursorValue =\n    cursor instanceof Date\n      ? cursor\n      : cursor != null\n        ? new Date(cursor)\n        : new Date();\n\n  if (direction === \"prev\") {\n    return {\n      cursorCondition: gt(cursorColumn, cursorValue),\n      orderBy: asc(cursorColumn),\n      needsReverse: true,\n    };\n  }\n\n  return {\n    cursorCondition: lt(cursorColumn, cursorValue),\n    orderBy: desc(cursorColumn),\n    needsReverse: false,\n  };\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/drizzle/sorting.ts",
      "content": "import { asc, desc, type SQL } from \"drizzle-orm\";\nimport type { ColumnMapping, SortDescriptor } from \"./types\";\n\n/**\n * Build an ORDER BY clause from a sort descriptor.\n * Returns undefined if no sort is specified or the column isn't mapped.\n */\nexport function buildOrderBy(\n  mapping: ColumnMapping,\n  sort: SortDescriptor,\n): SQL | undefined {\n  if (!sort) return undefined;\n\n  const column = mapping[sort.id];\n  if (!column) return undefined;\n\n  return sort.desc ? desc(column) : asc(column);\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:block"
}