{"id":28646792,"url":"https://github.com/iodesystems/dataset","last_synced_at":"2026-03-10T20:32:49.040Z","repository":{"id":57725370,"uuid":"169035722","full_name":"IodeSystems/dataset","owner":"IodeSystems","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-01T14:50:38.000Z","size":648,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-01T17:54:54.292Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/IodeSystems.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-02-04T06:18:01.000Z","updated_at":"2026-01-04T17:41:19.000Z","dependencies_parsed_at":"2023-11-09T01:44:41.645Z","dependency_job_id":"b6b7af10-d2c7-4ac9-8c0c-7a352429b1b8","html_url":"https://github.com/IodeSystems/dataset","commit_stats":null,"previous_names":["nthalk/dataset"],"tags_count":43,"template":false,"template_full_name":null,"purl":"pkg:github/IodeSystems/dataset","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IodeSystems%2Fdataset","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IodeSystems%2Fdataset/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IodeSystems%2Fdataset/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IodeSystems%2Fdataset/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/IodeSystems","download_url":"https://codeload.github.com/IodeSystems/dataset/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IodeSystems%2Fdataset/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30352877,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T15:55:29.454Z","status":"ssl_error","status_checked_at":"2026-03-10T15:54:58.440Z","response_time":106,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-13T02:07:00.376Z","updated_at":"2026-03-10T20:32:49.029Z","avatar_url":"https://github.com/IodeSystems.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"DataSet\n========================================\n\nA Kotlin library providing a type-safe, DSL-based query builder that converts freeform user queries into JOOQ SQL conditions, designed with an aim for least surprise.\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n**Version:** 9.0.1-SNAPSHOT\n**Requirements:** Kotlin 2.1+, Java 21+, JOOQ 3.19+\n\nExample query language:\n```clickhouse\nthis \"and that\", \"or these and those and\" someField:\"is this\"\n```\n\n## Features\n\n- **Type-safe query building** - Compile-time verification of field types and operations\n- **Natural language search parsing** - Convert user-friendly search queries to SQL using ANTLR4\n- **Declarative field configuration** - Define searchable/orderable fields inline with SELECT statements\n- **HTTP request integration** - Built-in support for pagination, ordering, and filtering from REST APIs\n- **Flexible mapping** - Transform database records to DTOs, Maps, or any custom type\n- **Automatic search error recovery** - Gracefully handles malformed search queries\n\n## Quick Start\n\nGiven a defined dataset schema, users can input freeform query\nlanguage to produce filtered result sets from a provided\ndataset.\n\nAll you have to do is:\n\n1. Define a DataSet from a JOOQ table using `DataSet { }`\n2. Configure fields as searchable/orderable inline\n3. Send a JSON DataSet.Request to an endpoint\n4. Render produced records and sizing information\n\nNo more custom SQL!\n\n## Search Query Semantics\n\n1. Unquoted terms are converted into tokens.\n2. Spaced terms are considered AND conditions.\n3. Commaed terms are considered OR conditions.\n4. Terms can be grouped with parentheses.\n5. Targets can be specialized with colons `target:value`\n6. Negation is supported with `!` prefix\n\n### Search Examples\n\n| Query | Meaning |\n|-------|---------|\n| `john` | Simple term search |\n| `john active` | john AND active |\n| `john, jane` | john OR jane |\n| `!inactive` | NOT inactive |\n| `(john, jane) status:active` | (john OR jane) AND status:active |\n| `email:john@example.com` | Targeted field search |\n\n\n## Complete Example\nGiven the following query:\n```clickhouse\nfrom:\"some name\" attachment:true (content:(user_input), user_input)\n```\n\nIt is expected to be converted into:\n```SQL\nWHERE \n    from:\"some name\"\n    AND attachment:true\n    AND (\n        content:user_input\n        OR any:user_input\n    )\n```\n\nAnd given the schema:\n```kotlin\nval query = DataSet {\n    search(\"daysAgo\") { s -\u003e\n        if (s.startsWith(\"\u003c\")) {\n            val ltDaysAgo = s.drop(1).toLongOrNull()\n            if (ltDaysAgo == null) null\n            else CREATED_AT.lessOrEqual(OffsetDateTime.now().minusDays(ltDaysAgo))\n        } else if (s.startsWith(\"\u003e\")) {\n            val gtDaysAgo = s.drop(1).toLongOrNull()\n            if (gtDaysAgo == null) null\n            else CREATED_AT.greaterOrEqual(OffsetDateTime.now().minusDays(gtDaysAgo))\n        } else {\n            val daysAgo = s.toLongOrNull()\n            if (daysAgo == null) null\n            else CREATED_AT.greaterOrEqual(OffsetDateTime.now().minusDays(daysAgo))\n        }\n    }\n    search(\"is_x\", open = true) { s -\u003e\n        if (s == \"x\") DSL.trueCondition()\n        else null\n    }\n\n    db.select(\n        field(EMAIL_ID) {\n            primaryKey()\n        },\n        field(ATTACHMENT) {\n            search(global = false) { f, s -\u003e\n                if (s.lowercase() == \"true\") f.isNull else null\n            }\n        },\n        field(CONTENT) {\n            orderable()\n            search { f, s -\u003e f.containsIgnoreCase(s) }\n        },\n        field(FROM) {\n            orderable()\n            search { f, s -\u003e f.containsIgnoreCase(s) }\n        },\n        CREATED_AT\n    ).from(EMAIL)\n}\nval request = DataSet.Request(\n    showColumns = true, showCounts = true, search = \"x\", partition = \"\"\n)\nval response = request.response(db, query)\n```\n\n## Field Configuration Options\n\n```kotlin\nfield(FIELD) {\n    primaryKey()                          // Mark as primary key\n    orderable()                           // Allow ordering (default ASC)\n    orderable(Direction.DESC)             // Allow ordering with default direction\n    search { f, s -\u003e f.eq(s) }           // Searchable, global (open search)\n    search(global = false) { f, s -\u003e ... } // Searchable, targeted only\n}\n```\n\n## Named Searches\n\nNamed searches are custom search functions that aren't tied to a specific field. They're perfect for complex logic like date ranges, price ranges, or multi-field conditions.\n\n### Basic Named Search\n\n```kotlin\nval query = DataSet {\n    search(\"daysAgo\") { s -\u003e\n        when {\n            s.startsWith(\"\u003c\") -\u003e {\n                s.drop(1).toLongOrNull()?.let { days -\u003e\n                    CREATED_AT.lessOrEqual(OffsetDateTime.now().minusDays(days))\n                }\n            }\n            s.startsWith(\"\u003e\") -\u003e {\n                s.drop(1).toLongOrNull()?.let { days -\u003e\n                    CREATED_AT.greaterOrEqual(OffsetDateTime.now().minusDays(days))\n                }\n            }\n            else -\u003e {\n                s.toLongOrNull()?.let { days -\u003e\n                    CREATED_AT.greaterOrEqual(OffsetDateTime.now().minusDays(days))\n                }\n            }\n        }\n    }\n\n    db.select(\n        field(ID) { primaryKey() },\n        field(NAME) { search { f, s -\u003e f.containsIgnoreCase(s) } },\n        CREATED_AT\n    ).from(PRODUCT)\n}\n\n// Usage:\nquery.search(\"daysAgo:\u003c30\")   // Products created less than 30 days ago\nquery.search(\"daysAgo:\u003e90\")   // Products created more than 90 days ago\n```\n\n### Advanced Named Search Examples\n\n```kotlin\nval query = DataSet {\n    // Price range search\n    search(\"price\") { s -\u003e\n        when {\n            s.startsWith(\"\u003e=\") -\u003e {\n                s.drop(2).toBigDecimalOrNull()?.let { PRICE.greaterOrEqual(it) }\n            }\n            s.startsWith(\"\u003c=\") -\u003e {\n                s.drop(2).toBigDecimalOrNull()?.let { PRICE.lessOrEqual(it) }\n            }\n            s.startsWith(\"\u003e\") -\u003e {\n                s.drop(1).toBigDecimalOrNull()?.let { PRICE.greaterThan(it) }\n            }\n            s.startsWith(\"\u003c\") -\u003e {\n                s.drop(1).toBigDecimalOrNull()?.let { PRICE.lessThan(it) }\n            }\n            s.contains(\"..\") -\u003e {\n                val parts = s.split(\"..\")\n                if (parts.size == 2) {\n                    val min = parts[0].toBigDecimalOrNull()\n                    val max = parts[1].toBigDecimalOrNull()\n                    if (min != null \u0026\u0026 max != null) {\n                        PRICE.between(min, max)\n                    } else null\n                } else null\n            }\n            else -\u003e {\n                s.toBigDecimalOrNull()?.let { PRICE.eq(it) }\n            }\n        }\n    }\n\n    // Status flag search\n    search(\"active\", open = false) { s -\u003e\n        when (s.lowercase()) {\n            \"true\", \"yes\" -\u003e STATUS.eq(\"ACTIVE\")\n            \"false\", \"no\" -\u003e STATUS.eq(\"INACTIVE\")\n            else -\u003e null\n        }\n    }\n\n    // Open search (participates in global search)\n    search(\"special\", open = true) { s -\u003e\n        if (s == \"x\") DSL.trueCondition()\n        else null\n    }\n\n    db.select(\n        field(ID) { primaryKey() },\n        field(NAME) { search { f, s -\u003e f.containsIgnoreCase(s) } },\n        STATUS,\n        PRICE,\n        CREATED_AT\n    ).from(PRODUCT)\n}\n\n// Usage examples:\nquery.search(\"price:\u003e=100\")           // Products $100 or more\nquery.search(\"price:\u003c50\")             // Products under $50\nquery.search(\"price:10..99\")          // Products between $10 and $99\nquery.search(\"active:true\")           // Active products only\nquery.search(\"laptop daysAgo:\u003c7\")     // Recent laptops\n```\n\n### Named Search Parameters\n\n- **`name`**: The search target name (e.g., `\"daysAgo\"` matches `daysAgo:\u003c30`)\n- **`open`**:\n  - `true` (default): Participates in global/unqualified searches\n  - `false`: Only matches when explicitly targeted (e.g., `active:true`)\n- **Return `null`**: If the search string doesn't match, return `null` to ignore the condition\n\nNamed searches must be defined **before** the `db.select()` call.\n\n## Lazy SQL (Runtime Conditions)\n\nSometimes you need conditions that are evaluated **at query execution time** rather than at DataSet creation time. This is essential for values that change per-request, like `currentUserId`, tenant IDs, or current timestamps.\n\n### When Conditions Are Applied\n\n**Static conditions** (evaluated once during DataSet construction):\n```kotlin\nval query = DataSet {\n    val tenantId = getCurrentTenantId()  // ❌ Evaluated once at startup!\n    db.select(\n        field(USER.ID) { primaryKey() }\n    ).from(USER)\n}.where(USER.TENANT_ID.eq(tenantId))  // Wrong: uses startup tenantId\n```\n\n**Lazy SQL** (evaluated at query execution):\n```kotlin\nval query = DataSet {\n    db.select(\n        field(USER.ID) { primaryKey() }\n    ).from(USER)\n    .where(lazy {\n        // ✅ Evaluated per-request!\n        USER.TENANT_ID.eq(getCurrentTenantId())\n    })\n}\n```\n\n### Use Cases\n\n**Multi-tenant applications:**\n```kotlin\n// DataSet created once at startup\nval userQuery = DataSet {\n    db.select(\n        field(USER.ID) { primaryKey() },\n        field(USER.NAME) { search { f, s -\u003e f.containsIgnoreCase(s) } }\n    ).from(USER)\n    .where(lazy {\n        USER.TENANT_ID.eq(RequestContext.currentTenantId())\n    })\n}\n\n// Later, in request handler:\nRequestContext.setTenantId(\"tenant-123\")\nval users = request.filter(db, userQuery)  // Only sees tenant-123 users\n```\n\n**User permissions:**\n```kotlin\nval productQuery = DataSet {\n    db.select(\n        field(PRODUCT.ID) { primaryKey() },\n        field(PRODUCT.NAME) { search { f, s -\u003e f.containsIgnoreCase(s) } }\n    ).from(PRODUCT)\n    .where(lazy {\n        PRODUCT.OWNER_ID.eq(SecurityContext.currentUserId())\n    })\n}\n```\n\n**Complex conditional logic:**\n```kotlin\nval query = DataSet {\n    db.select(\n        field(PRODUCT.ID) { primaryKey() }\n    ).from(PRODUCT)\n    .where(lazy {\n        val user = SecurityContext.currentUser()\n        if (user.isAdmin) {\n            DSL.trueCondition()  // Admins see everything\n        } else {\n            PRODUCT.OWNER_ID.eq(user.id)  // Users see only their products\n        }\n    })\n}\n```\n\n### Why Lazy SQL?\n\n- **Discoverable**: `lazy { }` is available in the DataSet DSL\n- **Type-safe**: Full JOOQ type checking\n- **Clear intent**: Obviously runtime-evaluated\n- **Flexible**: Supports any JOOQ Condition\n\n### Important Notes\n\n- Lazy SQL is evaluated **every time** the query executes (not cached)\n- Perfect for multi-tenant apps, user permissions, and time-based queries\n- Uses JOOQ internal APIs (still functional, monitored for breaking changes)\n\n## Mapping to DTOs\n\nTransform query results into custom types:\n\n```kotlin\ndata class UserDto(val id: Long, val email: String)\n\nval query = DataSet {\n    db.select(\n        field(USER.ID) {\n            primaryKey()\n            search { f, s -\u003e f.eq(s.toLongOrNull()) }\n        },\n        field(USER.EMAIL) {\n            search { f, s -\u003e f.containsIgnoreCase(s) }\n        }\n    ).from(USER)\n}.map { record -\u003e\n    UserDto(\n        id = record[USER.ID],\n        email = record[USER.EMAIL]\n    )\n}\n\nval users: List\u003cUserDto\u003e = query.data(db).fetch()\n```\n\n## Migration from v8.x\n\nIf you're upgrading from v8.x, here's what changed:\n\n**Before (v8.x):**\n```kotlin\n// Old DataSetBuilder API\nval query = DataSetBuilder.build {\n    db.select(\n        field(USER_ID) { primaryKey() }\n    ).from(USER)\n}\n\n// Or old Fields API\nval fields = DataSet.build {\n    field(USER_ID)\n}\nval query = fields.toDataSet { sql -\u003e sql.from(USER) }\n```\n\n**After (v9.0):**\n```kotlin\n// New unified API\nval query = DataSet {\n    db.select(\n        field(USER_ID) { primaryKey() }\n    ).from(USER)\n}\n```\n\n**Key changes:**\n- Single entry point: `DataSet { }` replaces all old builder patterns\n- Field configuration uses 2-parameter lambda: `search { f, s -\u003e ... }` instead of `search { s -\u003e field.method(s) }`\n- Named searches defined before select: `search(\"name\") { ... }`\n- Deprecated methods removed: `DataSet.forTable()`, `DataSet.build()`, `DataSetBuilder.build()`, `Fields.toDataSet()`\n\n## HTTP Integration: `filter()` vs `response()`\n\nDataSet.Request provides two methods for executing queries, each designed for different use cases:\n\n### `request.filter()` - Get Data Only\n\nUse `filter()` when you **only need the data** (no metadata, counts, or column information). Perfect for:\n- Bulk operations (delete, update)\n- Export operations\n- Simple list endpoints that don't need pagination metadata\n- Background jobs\n\n```kotlin\nval request = DataSet.Request(\n    search = \"status:active\",\n    page = 0,\n    pageSize = 50\n)\n\n// Returns: List\u003cT\u003e - just the data\nval data: List\u003cRecord\u003e = request.filter(db, query)\n\n// For unlimited results (bulk operations)\nval allData = request.filter(db, query, unlimit = true)\n```\n\n**What `filter()` does:**\n- ✅ Applies search, selection, ordering, pagination\n- ✅ Returns data only\n- ❌ No counts\n- ❌ No column metadata\n- ❌ No searchRendered\n\n### `request.response()` - Get Full Response\n\nUse `response()` when you need **complete metadata** for UI rendering. Perfect for:\n- List/table views with pagination\n- Endpoints that need total counts\n- UIs that need column metadata (searchable, orderable, types)\n- Search interfaces where you need to show corrected search\n\n```kotlin\nval request = DataSet.Request(\n    search = \"status:active\",\n    page = 0,\n    pageSize = 50,\n    showCounts = true,      // Include total counts\n    showColumns = true      // Include column metadata\n)\n\n// Returns: Response\u003cT\u003e with data, counts, columns, searchRendered\nval response: DataSet.Response\u003cRecord\u003e = request.response(db, query)\n\n// Access response components:\nval data = response.data                    // List\u003cT\u003e\nval totalCount = response.count?.inQuery    // Total matching records\nval columns = response.columns              // Column metadata for UI\nval correctedSearch = response.searchRendered // Show user what was searched\n```\n\n**What `response()` does:**\n- ✅ Applies search, selection, ordering, pagination\n- ✅ Returns data\n- ✅ Returns counts (if `showCounts = true`)\n- ✅ Returns column metadata (if `showColumns = true`)\n- ✅ Returns corrected search string\n\n### Quick Comparison\n\n| Feature | `filter()` | `response()` |\n|---------|-----------|----------|\n| Returns data | ✅ | ✅ |\n| Returns counts | ❌ | ✅ (optional) |\n| Returns column metadata | ❌ | ✅ (optional) |\n| Returns searchRendered | ❌ | ✅ |\n| Unlimit option | ✅ | ❌ |\n| Best for | Bulk ops, exports | UI tables, lists |\n\n### Example Use Cases\n\n**List Endpoint (use `response()`):**\n```kotlin\n@GetMapping(\"/api/products\")\nfun listProducts(@RequestBody request: DataSet.Request): DataSet.Response\u003cProductDto\u003e {\n    return request.response(db, productQuery)\n}\n```\n\n**Delete Selection (use `filter()` with selection):**\n```kotlin\n@DeleteMapping(\"/api/products\")\nfun deleteProducts(@RequestBody request: DataSet.Request): Int {\n    // Get selected records using selection\n    val toDelete = request.filter(db, productQuery, unlimit = true)\n\n    // Extract IDs and delete\n    val ids = toDelete.map { it[PRODUCT.ID] }\n    return db.deleteFrom(PRODUCT)\n        .where(PRODUCT.ID.`in`(ids))\n        .execute()\n}\n```\n\n**Example requests:**\n\nDelete specific products by ID:\n```json\n{\n  \"selection\": {\n    \"include\": true,\n    \"keys\": [[\"1\"], [\"2\"], [\"5\"]]\n  }\n}\n```\n\nDelete all except specific products:\n```json\n{\n  \"selection\": {\n    \"include\": false,\n    \"keys\": [[\"3\"], [\"4\"]]\n  }\n}\n```\n\nDelete all matching a search:\n```json\n{\n  \"search\": \"status:inactive\"\n}\n```\n\n**Export (use `filter()` with unlimit):**\n```kotlin\n@GetMapping(\"/api/products/export\")\nfun exportProducts(@RequestBody request: DataSet.Request): ByteArray {\n    val allData = request.filter(db, productQuery, unlimit = true)\n    return csvExporter.export(allData)\n}\n```\n\n## Selection\n\nSelection allows filtering by specific primary key values. Perfect for \"delete selected rows\" or \"export selected rows\" features in UIs.\n\n### Selection Object\n\n```kotlin\ndata class Selection(\n    val include: Boolean,        // true = include only these; false = exclude these\n    val keys: List\u003cList\u003cString\u003e\u003e // List of primary key value rows\n)\n```\n\n### How Selection Works\n\nSelection filters by primary key values:\n\n1. **Single Primary Key**: Each key row has one value\n   ```json\n   {\n     \"selection\": {\n       \"include\": true,\n       \"keys\": [[\"1\"], [\"2\"], [\"5\"]]\n     }\n   }\n   ```\n   SQL: `WHERE ID IN (1, 2, 5)`\n\n2. **Composite Primary Key**: Each key row has multiple values\n   ```json\n   {\n     \"selection\": {\n       \"include\": true,\n       \"keys\": [\n         [\"user1\", \"tenant-a\"],\n         [\"user2\", \"tenant-b\"]\n       ]\n     }\n   }\n   ```\n   SQL: `WHERE (USER_ID = 'user1' AND TENANT_ID = 'tenant-a') OR (USER_ID = 'user2' AND TENANT_ID = 'tenant-b')`\n\n3. **Exclude Mode**: Invert the selection\n   ```json\n   {\n     \"selection\": {\n       \"include\": false,\n       \"keys\": [[\"3\"]]\n     }\n   }\n   ```\n   SQL: `WHERE NOT (ID = 3)`\n\n### Combining Selection with Search\n\nSelection can be combined with search for powerful filtering:\n\n```json\n{\n  \"search\": \"status:active\",\n  \"selection\": {\n    \"include\": true,\n    \"keys\": [[\"1\"], [\"2\"], [\"5\"]]\n  }\n}\n```\n\nThis finds records that are BOTH active AND have ID 1, 2, or 5.\n\n### Primary Key Configuration\n\nSelection requires fields to be marked as `primaryKey()`:\n\n```kotlin\nval query = DataSet {\n    db.select(\n        field(PRODUCT.ID) {\n            primaryKey()  // Required for selection\n            search { f, s -\u003e f.eq(s.toLongOrNull()) }\n        },\n        field(PRODUCT.NAME) {\n            search { f, s -\u003e f.containsIgnoreCase(s) }\n        }\n    ).from(PRODUCT)\n}\n```\n\nFor composite keys, mark all key fields:\n\n```kotlin\nval query = DataSet {\n    db.select(\n        field(USER.ID) {\n            primaryKey()  // First part of composite key\n            search { f, s -\u003e f.eq(s) }\n        },\n        field(TENANT.ID) {\n            primaryKey()  // Second part of composite key\n            search { f, s -\u003e f.eq(s) }\n        },\n        field(USER.NAME) {\n            search { f, s -\u003e f.containsIgnoreCase(s) }\n        }\n    ).from(USER)\n}\n```\n\n**Key ordering:** The order of `keys` array values must match the order of `primaryKey()` fields in your DataSet definition.\n\n### UI Integration Example\n\nTypical frontend workflow:\n\n1. User views paginated table with checkboxes\n2. User selects specific rows (e.g., products with IDs 1, 5, 7)\n3. User clicks \"Delete Selected\"\n4. Frontend sends:\n   ```json\n   {\n     \"selection\": {\n       \"include\": true,\n       \"keys\": [[\"1\"], [\"5\"], [\"7\"]]\n     }\n   }\n   ```\n5. Backend deletes only those specific records\n\nOr for \"Select All\" with exclusions:\n\n1. User clicks \"Select All\" (selects all 100 products)\n2. User unchecks 2 products (IDs 3 and 8)\n3. User clicks \"Delete Selected\"\n4. Frontend sends:\n   ```json\n   {\n     \"selection\": {\n       \"include\": false,\n       \"keys\": [[\"3\"], [\"8\"]]\n     }\n   }\n   ```\n5. Backend deletes all except IDs 3 and 8\n\n## User Query Errors\n------------------------------------\nBecause this is a regular language, and users may supply bad data,\nthere needs to be a method of rescuing parse errors.\n\nAny time there is a parse error, the offending token is escaped.\n\nThis works out in the grammar to convert:\n\n```clickhouse\nfrom:((, (this is parser torture\"\"\\\")\n```\n\nto\n\n```clickhouse\nfrom:(\\(, \\(this is parser torture\\\"\\\"\\\")\n```\n\nWhile the user may not end up rendering what they think,\nthe (last applied) search is returned in the dataset result.\n\nIn the case of partitioning or sub querying, we simply send back an empty string.\n\nMake sure to update the user's search input with `searchRendered`\nreturn value to ensure that the user knows what produced their result.\n\n## Building from Source\n\n```bash\n./gradlew build\n./gradlew test\n```\n\n## Links\n\n- **Repository:** https://github.com/iodesystems/dataset\n- **Issues:** https://github.com/iodesystems/dataset/issues\n- **JOOQ Documentation:** https://www.jooq.org/doc/\n- **ANTLR4 Documentation:** https://github.com/antlr/antlr4/blob/master/doc/index.md\n\n## License\n\nMIT License\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiodesystems%2Fdataset","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiodesystems%2Fdataset","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiodesystems%2Fdataset/lists"}