Skip to content

feat(where): Prisma-like case-insensitive mode for core and CLI #33

Description

@teles

Context

Today HolySheets does not expose a Prisma-like case-insensitive mode for string filters.

Current behavior:

  • Core local filtering (WhereService + whereFilters) is case-sensitive for equals, not, contains, startsWith, endsWith.
  • search in core is currently case-insensitive by implementation detail (RegExp(..., 'i')).
  • CLI (public mode) builds GViz queries and also has no explicit case mode option.
  • Google Visualization Query Language does not provide a native mode parameter; case-insensitive behavior must be implemented via expression strategy (e.g. lower(column) patterns).

Reference and product direction:

  • We want a Prisma-like API shape where case mode is defined per string filter, with explicit mode: 'insensitive'.

Goal

Implement a consistent, Prisma-inspired case-sensitivity API across:

  • Core API (where object)
  • CLI (--where-* flags)

with predictable behavior and docs/tests.

Proposed API (Prisma-like)

Core

Allow per-filter object with value + mode for string operators.

Example:

await reader.findMany({
  where: {
    name: { contains: { value: 'saur', mode: 'insensitive' } },
    type: { equals: { value: 'GRASS', mode: 'insensitive' } },
    city: { startsWith: { value: 'san', mode: 'default' } }
  }
})

Suggested mode values:

  • default (or omitted): current behavior
  • insensitive

Backwards compatibility:

  • Existing syntax must keep working:
    • { name: { contains: 'saur' } }
    • { name: 'Pikachu' }

CLI

Add --where-mode <default|insensitive> scoped to the current --where-field block.

Example:

holysheets read find-many \
  --spreadsheet-id <ID> \
  --sheet pokemon \
  --where-field name --where-op contains --where-value saur --where-mode insensitive \
  --where-field type --where-op equals --where-value GRASS --where-mode insensitive

Grouping rule:

  • --where-field starts a block
  • --where-op, --where-value, and new --where-mode apply to latest block

Default:

  • If omitted, mode is default

Implementation Notes

Core filtering engine

  • Extend where types to support string filter object payloads:
    • Current: contains: string
    • New: contains: string | { value: string; mode?: 'default' | 'insensitive' }
  • Apply normalization when mode === 'insensitive' (string comparisons via normalized casing)
  • Keep numeric and array operators unchanged (lt, in, etc.)
  • Decide and document search behavior explicitly:
    • Option A: support mode as well (search: { value: '...regex...', mode: ... })
    • Option B: keep current behavior and disallow mode for search in v1

CLI + normalization

  • Parse and validate new --where-mode
  • Include mode in normalized where representation
  • Validate only allowed values
  • Keep old CLI syntax valid

GViz query builder

  • For string operators with mode=insensitive, generate expressions using lowercasing strategy, e.g.:
    • lower(A) contains 'saur'
    • lower(A) starts with 'pi'
    • lower(A) = 'grass'
  • Ensure escaping still works
  • Confirm behavior for operators unsupported with lowercasing and document decisions

Docs to update

  • docs/en/concepts/where-filters.md
  • docs/en/cli/filters-and-select.md
  • docs/en/cli/complete-guide.md
  • README.md
  • Any type/reference docs that describe WhereClause

Include examples in both:

  • Core usage (TS)
  • CLI usage (--where-mode insensitive)

Tests

Add/adjust tests for:

  • Core where filter behavior with mode: 'insensitive'
  • Backward compatibility with current syntax
  • CLI parse/normalize for --where-mode
  • GViz query builder output for insensitive mode
  • Validation errors for invalid modes

Acceptance Criteria

  • Core supports Prisma-like per-filter mode: 'insensitive' for string operators
  • CLI supports --where-mode per where block
  • Default behavior remains backward-compatible
  • Public mode query generation handles insensitive mode correctly
  • Documentation updated with examples and caveats
  • Automated tests cover new behavior and compatibility

Open Questions

  1. Should search receive mode in v1, or keep current semantics?
  2. Should simple string shorthand (where: { name: 'Pikachu' }) ever support insensitive mode, or stay strictly default-sensitive?
  3. Exact naming: use default + insensitive (closest to Prisma), or only insensitive boolean-like semantics?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions