Skip to content

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

@teles

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions