| name | odata-query-optimization |
| description | OData performance patterns for @proofkit/fmodata. Covers defaultSelect schema vs all, select() for minimal field fetching, select("all") override, pagination with top/skip, default 1000 record limit, batch operations for reducing round trips, entity IDs FMFID FMTID for rename resilience, null field query performance, getQueryString() debugging, relationship query performance testing, FileMaker OData optimization, avoiding OData service overload during testing.
|
| metadata | {"type":"core","library":"proofkit","library_version":"0.1.1"} |
| requires | ["fmodata-client"] |
| sources | ["proofsh/proofkit:packages/fmodata/src/client/builders/query-builder.ts","proofsh/proofkit:packages/fmodata/src/client/entity-set.ts","proofsh/proofkit:packages/fmodata/src/orm/table.ts","proofsh/proofkit:apps/docs/content/docs/fmodata/*.mdx"] |
Setup
This skill assumes a working fmodata client (see fmodata-client). Optimization starts at the schema level with defaultSelect and continues per-query with select().
defaultSelect on schema
import { fmTableOccurrence, textField, numberField } from "@proofkit/fmodata";
const users = fmTableOccurrence("users", {
id: textField().primaryKey().entityId("FMFID:12039485"),
name: textField().notNull().entityId("FMFID:34323433"),
email: textField().entityId("FMFID:12232424"),
age: numberField().entityId("FMFID:43234355"),
}, {
defaultSelect: "schema",
entityId: "FMTID:12432533",
});
const usersAll = fmTableOccurrence("users", {
id: textField().primaryKey(),
name: textField().notNull(),
}, {
defaultSelect: "all",
});
Per-query select override
const result = await db.from(users).list()
.select({ name: users.name, email: users.email })
.execute();
const result = await db.from(users).list()
.select("all")
.execute();
Core Patterns
1. Selective fetching with select()
Fewer fields = smaller response = faster queries. Always prefer explicit select() when you need a subset of fields.
const result = await db.from(users).list()
.select({ name: users.name, email: users.email })
.execute();
const result = await db.from(users).list()
.select({ userName: users.name, userEmail: users.email })
.execute();
import { getTableColumns } from "@proofkit/fmodata";
const { age, ...cols } = getTableColumns(users);
const result = await db.from(users).list().select(cols).execute();
2. Pagination with top/skip/count
list() applies .top(1000) by default. Override for smaller pages or to fetch more.
const page1 = await db.from(users).list().top(50).skip(0).execute();
const page2 = await db.from(users).list().top(50).skip(50).execute();
const countResult = await db.from(users).list().count().execute();
const allRecords = await db.from(users).list().top(5000).execute();
3. Batch operations for reducing round trips
Combine multiple queries into a single HTTP request. Write operations are transactional.
const contactsQuery = db.from(contacts).list().top(5);
const usersQuery = db.from(users).list().top(5);
const result = await db.batch([contactsQuery, usersQuery]).execute();
const [contactsResult, usersResult] = result.results;
if (contactsResult.data) { }
if (usersResult.data) { }
const result = await db.batch([
db.from(contacts).list().top(10),
db.from(contacts).insert({ name: "Alice", email: "alice@example.com" }),
db.from(users).update({ active: true }).byId("user-123"),
]).execute();
4. Entity IDs for rename resilience
Entity IDs (FMTID/FMFID) prevent breakage when FileMaker fields or table occurrences are renamed.
IMPORTANT: Entity IDs must come from FileMaker metadata via @proofkit/typegen. Do NOT invent FMFID/FMTID values — the IDs shown below are illustrative only. Guessed IDs cause silent query failures.
const users = fmTableOccurrence("users", {
id: textField().primaryKey().entityId("FMFID:12039485"),
name: textField().notNull().entityId("FMFID:34323433"),
}, {
entityId: "FMTID:12432533",
});
const db = connection.database("MyDatabase", { useEntityIds: true });
const result = await db.from(users).list().execute({ useEntityIds: true });
5. Debugging with getQueryString()
Inspect the generated OData URL without executing the request.
import { eq, asc } from "@proofkit/fmodata";
const queryString = db.from(users).list()
.select({ name: users.name, email: users.email })
.where(eq(users.active, true))
.orderBy(asc(users.name))
.top(10)
.getQueryString();
console.log(queryString);
const entityIdQuery = db.from(users).list()
.getQueryString({ useEntityIds: false });
Common Mistakes
[HIGH] Not accounting for default 1000 record limit
Wrong:
const result = await db.from(users).list().execute();
Correct:
const result = await db.from(users).list().top(5000).execute();
let allRecords = [];
let skip = 0;
const pageSize = 1000;
while (true) {
const page = await db.from(users).list().top(pageSize).skip(skip).execute();
if (!page.data || page.data.length === 0) break;
allRecords.push(...page.data);
if (page.data.length < pageSize) break;
skip += pageSize;
}
list() internally calls .top(1000) as a safety limit. If you need more records, override with an explicit .top() or paginate.
Source: packages/fmodata/src/client/entity-set.ts (line 185), packages/fmodata/src/client/query/query-builder.ts (line 50)
[MEDIUM] includeSpecialColumns silently dropped with explicit select()
Wrong:
const db = connection.database("MyDatabase", { includeSpecialColumns: true });
const result = await db.from(users).list()
.select({ name: users.name })
.execute();
Correct:
const result = await db.from(users).list()
.select(
{ name: users.name },
{ ROWID: true, ROWMODID: true }
)
.execute();
const result = await db.from(users).list().execute();
Per OData spec, special columns are only included when no $select query parameter is applied. Using .select() generates a $select, so special columns must be explicitly requested via the second argument.
Source: apps/docs/content/docs/fmodata/extra-properties.mdx
[MEDIUM] Entity IDs on tables without configured IDs
Wrong:
const users = fmTableOccurrence("users", {
id: textField().primaryKey(),
name: textField(),
});
const result = await db.from(users).list().execute({ useEntityIds: true });
Correct:
const users = fmTableOccurrence("users", {
id: textField().primaryKey().entityId("FMFID:12039485"),
name: textField().entityId("FMFID:34323433"),
}, {
entityId: "FMTID:12432533",
});
const result = await db.from(users).list().execute({ useEntityIds: true });
Entity IDs require both a table-level entityId (FMTID) and per-field .entityId() (FMFID) to be set. These must be generated by @proofkit/typegen from FileMaker metadata — do not manually add or invent entity IDs.
Source: apps/docs/content/docs/fmodata/entity-ids.mdx, packages/fmodata/src/orm/table.ts
[HIGH] Filtering on null fields causes severe performance degradation
Wrong:
import { isNull } from "@proofkit/fmodata";
const result = await db.from(users).list()
.where(isNull(users.deletedAt))
.execute();
Correct:
import { eq } from "@proofkit/fmodata";
const result = await db.from(users).list()
.where(eq(users.isActive, 1))
.execute();
FileMaker's OData implementation has severe performance issues when filtering on null/empty fields. Create a calculation field in FileMaker that evaluates the null condition and filter on that instead.
Source: FileMaker OData performance testing (known limitation)
[HIGH] Overwhelming OData service during testing
Wrong:
for (const id of userIds) {
const result = await db.from(users).get(id).execute();
}
Correct:
const queries = userIds.map(id => db.from(users).get(id));
const result = await db.batch(queries).execute();
for (let skip = 0; skip < total; skip += 100) {
const result = await db.from(users).list().top(100).skip(skip).execute();
await new Promise(resolve => setTimeout(resolve, 100));
}
Rapid sequential queries can degrade the FileMaker OData service. Use batch operations to combine multiple requests, and add throttling when iterating through large datasets during testing or migration.
Source: FileMaker OData service behavior (known limitation)
[MEDIUM] Not testing relationship query performance
Wrong:
const result = await db.from(invoices).list()
.expand(lineItems)
.top(500)
.execute();
Correct:
const test = await db.from(invoices).list()
.expand(lineItems)
.top(10)
.execute();
const queryString = db.from(invoices).list()
.expand(lineItems)
.top(10)
.getQueryString();
console.log(queryString);
const invoiceResult = await db.from(invoices).list().top(100).execute();
const lineItemResult = await db.from(lineItems).list()
.where(inArray(lineItems.invoiceId, invoiceIds))
.execute();
Relationship query performance via expand() is unpredictable in FileMaker's OData implementation. Always test with production-sized data. If performance is poor, fetch related records in separate queries or use batch operations.
Source: FileMaker OData relationship performance (must be tested case-by-case)
References
- fmodata-client — Client setup, connection configuration, database initialization. Understanding the client is prerequisite to applying these optimization patterns.