# GASsma - Complete Technical Specification > GASsma is a Google Apps Script (GAS) spreadsheet library that provides Prisma-like syntax for treating Google Sheets as databases. This document contains the complete technical specification including all edge cases, special behaviors, and implementation details derived from extensive test analysis. ## Table of Contents 1. [Installation and Setup](#installation-and-setup) 2. [Client Initialization](#client-initialization) 3. [Data Types and Type System](#data-types-and-type-system) 4. [Query Operations - Complete Reference](#query-operations---complete-reference) 5. [Filter Conditions - Detailed Behaviors](#filter-conditions---detailed-behaviors) 6. [Logical Operators - Advanced Usage](#logical-operators---advanced-usage) 7. [Data Manipulation Operations](#data-manipulation-operations) 8. [Aggregation and Analytics](#aggregation-and-analytics) 9. [GroupBy Operations](#groupby-operations) 10. [Error Handling and Edge Cases](#error-handling-and-edge-cases) 11. [Special Behaviors and Gotchas](#special-behaviors-and-gotchas) --- ## Installation and Setup **NPM Installation:** `npm i gassma` **Script ID for GAS:** `1ZVuWMUYs4hVKDCcP3nVw74AY48VqLm50wRceKIQLFKL0wf4Hyou-FIBH` **Current Version:** 0.8.0 ### Environment Support - Google Apps Script (native) - TypeScript (full type safety) - JavaScript (vanilla) - Node.js (via Clasp) --- ## Client Initialization ```typescript import { Gassma } from "gassma"; // Option 1: Specific spreadsheet ID const gassma = new Gassma.GassmaClient("spreadsheet-id"); // Option 2: Active spreadsheet (GAS environment) const gassma = new Gassma.GassmaClient(); // Access sheet const sheet = gassma.sheets.SheetName; // Configure data range sheet.changeSettings(startRow, startCol, endCol); ``` ### changeSettings Parameters - `startRowNumber`: number - Row to start reading data (1-based) - `startColumnValue`: number | string - Starting column (1-based number or letter) - `endColumnValue`: number | string - Ending column (1-based number or letter) --- ## Data Types and Type System ### Core Type Definitions ```typescript type GassmaAny = string | number | boolean | Date | null; type AnyUse = { [key: string]: GassmaAny }; type GassmaSheet = { [key: string]: GassmaController }; ``` ### Type Handling Rules 1. **Supported primitive types**: string, number, boolean, Date, null 2. **Null handling**: `null` and `undefined` are distinct 3. **Type preservation**: Original types maintained in results 4. **Type coercion**: Automatic in certain contexts (documented per operation) --- ## Query Operations - Complete Reference ### findFirst(findData: FindData) Returns single record or `null`. **Parameters:** ```typescript interface FindData { where?: WhereUse; select?: Select; omit?: Omit; orderBy?: OrderBy | OrderBy[]; skip?: number; } ``` **Edge Cases:** - With `skip`: Applies skip first, then returns first of remaining - Empty result: Returns `null` - No where clause: Returns first record in sheet ### findMany(findData: FindData) Returns array of records. **Additional Parameters:** ```typescript { take?: number; // Limit results distinct?: string | string[]; // Unique values } ``` **Edge Cases:** - Empty result: Returns `[]` - `take: 0`: Returns `[]` - Negative `skip`: Treated as 0 - Decimal `skip`: Floored to integer - `skip` exceeding total: Returns `[]` ### Query Execution Order **CRITICAL**: Operations execute in this specific order: 1. `where` - Filter records 2. `orderBy` - Sort filtered results 3. `skip` - Skip N records 4. `take` - Limit to N records 5. `select`/`omit` - Shape output fields 6. `distinct` - Remove duplicates --- ## Filter Conditions - Detailed Behaviors ### equals **Behavior:** - Exact match comparison - **Special**: Empty string `""` converted to `null` before comparison ```typescript where: { field: "value" } // Shorthand where: { field: { equals: "value" } } // Explicit where: { field: { equals: "" } } // Matches null values ``` ### not **Behavior:** - Negation of equals - **Special**: Empty string `""` converted to `null` before negation ```typescript where: { field: { not: "value" } } where: { field: { not: "" } } // Excludes null values ``` ### in / notIn **Critical Edge Case**: When cell value is `null`, both return `false` ```typescript // If cell value is null: where: { field: { in: ["a", "b"] } } // Returns false where: { field: { notIn: ["a", "b"] } } // Also returns false! ``` **Normal Behavior:** - `in`: Value must be in array - `notIn`: Value must not be in array ### String Matchers (contains, startsWith, endsWith) **Type Conversion**: Numbers automatically converted to strings ```typescript // With numeric field value 123: where: { field: { contains: "2" } } // Matches (123 → "123") where: { field: { startsWith: "1" } } // Matches where: { field: { endsWith: "3" } } // Matches ``` **Null Handling**: All return `false` when field is `null` ### Comparison Operators (lt, lte, gt, gte) **Boundaries:** - `lt`: Strictly less than (excludes equal) - `lte`: Less than or equal (includes equal) - `gt`: Strictly greater than (excludes equal) - `gte`: Greater than or equal (includes equal) **Null Handling**: All return `false` when field is `null` ### Multiple Conditions **Implicit AND**: Multiple conditions in same object ```typescript where: { age: 28, // AND city: "Tokyo" // Must both be true } ``` --- ## Logical Operators - Advanced Usage ### AND Operator **Format Flexibility:** ```typescript // Array format (recommended) where: { AND: [ { field1: "value1" }, { field2: "value2" } ] } // Object format (auto-converted to array internally) where: { AND: { field1: "value1", field2: "value2" } } ``` ### OR Operator **Important**: When combined with AND, OR intersects with AND results ```typescript where: { AND: [{ city: "Tokyo" }], // First: Find Tokyo residents OR: [{ job: "Engineer" }] // Then: Find Engineers ONLY among Tokyo residents } // Result: Tokyo Engineers (not all Engineers) ``` ### NOT Operator **Array Processing**: Sequential evaluation ```typescript where: { NOT: [ { status: "deleted" }, { archived: true } ] } ``` **Intersection Behavior**: When combined with AND, intersects with existing results ### Complex Nesting Fully supports nested logical operations: ```typescript where: { AND: [ { OR: [ { department: "Engineering" }, { department: "Design" } ] }, { NOT: { status: "inactive" } } ] } ``` --- ## Data Manipulation Operations ### create(createData: CreateData) **Field Handling:** - Missing fields in data: Returned as `null`, stored as `""` in sheet - Non-existent columns: Ignored (not added to sheet) - Returns: Complete object with all fields ```typescript const result = sheet.create({ data: { name: "John", age: 25 } }); // If sheet has columns [name, age, email]: // Returns: { name: "John", age: 25, email: null } // Stored: ["John", 25, ""] ``` ### createMany(createManyData: CreateManyData) **Edge Cases:** - Empty array: Returns `undefined` - Returns: `{ count: number }` ```typescript sheet.createMany({ data: [] // Returns undefined }); sheet.createMany({ data: [{ name: "John" }, { name: "Jane" }] }); // Returns: { count: 2 } ``` ### updateMany(updateData: UpdateData) **Behavior:** - Partial updates: Only specified fields changed - Non-existent fields: Ignored - Returns: `{ count: number }` of affected records ```typescript sheet.updateMany({ where: { department: "Sales" }, data: { salary: 50000 } // Only salary updated }); ``` ### upsertMany(upsertData: UpsertData) **Logic:** 1. If where condition matches: Apply update 2. If no match: Create new record with create data ```typescript sheet.upsertMany({ where: { email: "john@example.com" }, update: { lastLogin: new Date() }, create: { email: "john@example.com", name: "John", lastLogin: new Date() } }); ``` ### deleteMany(deleteData: DeleteData) **Behavior:** - Deletes all matching records - Returns: `{ count: number }` of deleted records - No matches: Returns `{ count: 0 }` --- ## Aggregation and Analytics ### aggregate(aggregateData: AggregateData) **Available Functions:** - `_avg`: Average (numbers only) - `_count`: Count of records - `_max`: Maximum value - `_min`: Minimum value - `_sum`: Sum (numbers only) ### Data Type Requirements **_avg and _sum:** - **Requirement**: All non-null values must be numeric - **Error Cases:** - Mixed types → `GassmaAggregateAvgError` / `GassmaAggregateSumError` - Non-numeric uniform type → `GassmaAggregateAvgTypeError` / `GassmaAggregateSumTypeError` - **Null handling**: Automatically filtered out **_max and _min:** - Work with: numbers, strings, booleans, Dates - String comparison: Alphabetical - Boolean: false < true - Date: Earlier < Later **_count:** - Always returns number (never null) - Empty dataset: Returns 0 ### Edge Cases **Empty Results:** ```typescript // All return null except _count sheet.aggregate({ where: { nonExistentField: "value" }, _avg: { salary: true }, // Returns null _count: { id: true } // Returns 0 }); ``` **Null Value Filtering:** ```typescript // Data: [10, null, 20, null, 30] sheet.aggregate({ _avg: { value: true } // Returns 20 (average of 10, 20, 30) }); ``` --- ## GroupBy Operations ### Basic GroupBy ```typescript sheet.groupBy({ by: ["department"], // Can be string or string[] _count: { id: true }, _avg: { salary: true } }); ``` ### Having Clause Filters groups after aggregation: ```typescript sheet.groupBy({ by: ["department"], _count: { id: true }, having: { _count: { id: { gte: 5 } // Only departments with 5+ employees } } }); ``` ### Complex Having Conditions Supports logical operators in having: ```typescript having: { OR: [ { _count: { id: { gte: 10 } } }, { _avg: { salary: { gte: 50000 } } } ] } ``` ### GroupBy Processing Order 1. Apply where filter 2. Apply orderBy 3. Apply skip/take to input data (not groups!) 4. Group remaining records 5. Calculate aggregations per group 6. Apply having filter 7. Return filtered groups **Important**: `skip` and `take` affect input records, not final groups --- ## Error Handling and Edge Cases ### Error Types **GassmaFindSelectOmitConflictError** - Message: "Cannot use both select and omit in the same query" - Trigger: Using both `select` and `omit` in same query **GassmaAggregateAvgError** - Message: "avg can only be used on one column" - Trigger: Multiple columns in _avg **GassmaAggregateAvgTypeError** - Message: Details about type mismatch - Trigger: Non-numeric data in _avg **GassmaAggregateSumError** / **GassmaAggregateSumTypeError** - Similar to avg errors but for sum operation ### Boundary Value Handling **Numeric Boundaries:** - Maximum: `Number.MAX_SAFE_INTEGER` - Minimum: `Number.MIN_SAFE_INTEGER` - Overflow: Not explicitly handled **Array Sizes:** - Empty arrays: Handled gracefully - Large arrays: Limited by memory **String Lengths:** - Empty strings: Special handling in filters (→ null) - Long strings: Limited by Google Sheets cell limits --- ## Special Behaviors and Gotchas ### Empty String to Null Conversion **CRITICAL**: Only happens in filter conditions, NOT in data operations ```typescript // Filter context - empty string becomes null where: { field: "" } // Matches null values // Data context - empty string preserved sheet.create({ data: { field: "" } }) // Stores empty string ``` ### Case Sensitivity All operations are case-sensitive: - String matching - Field names - Sheet names ### Performance Considerations **Memory Usage:** - Entire dataset loaded into memory - Large datasets may cause performance issues **Optimization Tips:** 1. Use `where` to filter early 2. Use `select` to reduce field count 3. Use `take` to limit results 4. Avoid unnecessary `distinct` on large datasets ### Order of Operations Matters ```typescript // Different results: sheet.findMany({ orderBy: { age: "asc" }, take: 10, distinct: "department" }); // vs sheet.findMany({ distinct: "department", orderBy: { age: "asc" }, take: 10 }); // Execution order is fixed regardless of property order in object ``` ### Select vs Omit Patterns **Use Select when:** - You need few specific fields - Working with large records - API response shaping **Use Omit when:** - You need most fields - Excluding sensitive data - Removing computed fields ### Multiple OrderBy Fields ```typescript orderBy: [ { department: "asc" }, // Primary sort { salary: "desc" }, // Secondary sort { name: "asc" } // Tertiary sort ] ``` When values are equal in earlier sort fields, later fields determine order. ### Distinct Behavior **Single Field:** ```typescript distinct: "department" // Returns first occurrence of each department ``` **Multiple Fields:** ```typescript distinct: ["department", "level"] // Unique combinations ``` **Processing:** Applied after all other operations (including select/omit) ### Date Handling Dates are compared as JavaScript Date objects: - Earlier dates are "less than" later dates - Date strings should be parsed to Date objects - Timezone considerations apply ### Boolean Value Ordering In sorting and comparisons: - `false` < `true` - Treated as 0 and 1 respectively ### Null Value Philosophy **General Rules:** 1. Nulls filtered from aggregations 2. Nulls return false in comparisons 3. Nulls preserved in data operations 4. Empty strings → null only in filters ### Transaction Support **Not Supported**: Operations are not transactional - No rollback capability - Partial failures possible in batch operations - Consider implementing application-level transactions ### Concurrency **Not Thread-Safe**: Google Sheets operations are not atomic - Race conditions possible with concurrent writes - Consider implementing locking mechanisms for critical operations --- ## Advanced Patterns and Best Practices ### Pagination Pattern ```typescript const pageSize = 20; const page = 3; sheet.findMany({ skip: (page - 1) * pageSize, take: pageSize }); ``` ### Soft Delete Pattern ```typescript // Instead of deleteMany, use updateMany sheet.updateMany({ where: { id: recordId }, data: { deletedAt: new Date() } }); // Query non-deleted records sheet.findMany({ where: { deletedAt: null } }); ``` ### Audit Trail Pattern ```typescript sheet.create({ data: { ...userData, createdAt: new Date(), createdBy: Session.getActiveUser().getEmail() } }); ``` ### Bulk Operations Pattern ```typescript // Batch creates for performance const records = largeDataset.map(item => ({ ...item, processedAt: new Date() })); sheet.createMany({ data: records }); ``` ### Complex Query Pattern ```typescript // Find active engineers in Tokyo or Osaka with high performance sheet.findMany({ where: { AND: [ { status: "active" }, { role: "engineer" }, { OR: [ { city: "Tokyo" }, { city: "Osaka" } ] }, { performanceScore: { gte: 80 } } ] }, orderBy: [ { performanceScore: "desc" }, { name: "asc" } ], take: 10 }); ``` --- ## Testing Insights Based on comprehensive test analysis: 1. **100+ test files** covering all operations 2. **544 test cases** validating edge cases 3. **Extensive null handling** tests 4. **Type safety** validation across operations 5. **Complex query** combination testing 6. **Error condition** verification 7. **Boundary value** testing 8. **Performance** benchmarking with large datasets --- ## API Completeness The library provides complete CRUD + analytics operations matching modern ORMs: - ✅ Create (single and batch) - ✅ Read (with complex filtering) - ✅ Update (partial and batch) - ✅ Delete (batch with conditions) - ✅ Upsert (update or create) - ✅ Aggregation (avg, sum, min, max, count) - ✅ Grouping (with having clause) - ✅ Pagination (skip/take) - ✅ Sorting (multi-field) - ✅ Field selection (select/omit) - ✅ Distinct values - ✅ Complex logical operations (AND/OR/NOT) - ✅ Type-safe operations with TypeScript --- ## Version History **v0.8.0** (Current) - Full feature set as documented - Stable API - Production ready --- ## Links and Resources - [Official Reference](https://akahoshi1421.github.io/gassma-reference/) - [GitHub Repository](https://github.com/akahoshi1421/gassma) - [NPM Package](https://www.npmjs.com/package/gassma) - [Script ID for GAS](https://script.google.com/macros/library/d/1ZVuWMUYs4hVKDCcP3nVw74AY48VqLm50wRceKIQLFKL0wf4Hyou-FIBH/1)