# Migration guide

This guide helps you upgrade from dcupl SDK 1.x (1.11.x) to 2.0. 2.0 keeps every 1.x method, but some behaviour changed: check [Behaviour Changes from 1.x](#behaviour-changes-from-1x) first. The new query API method names are optional; the older names still work in 2.0.

## Behaviour changes from 1.x

Upgrade `@dcupl/core`, `@dcupl/common` and `@dcupl/loader` together, to the same 2.0 version. If your app uses dcupl Connect ([Debugging](/sdk/guides/debugging)), upgrade `@dcupl/connect` with them: its 1.x release depends on `@dcupl/core` 1.11.

```bash
npm install @dcupl/core @dcupl/common @dcupl/loader
npm install @dcupl/connect # only with dcupl Connect
```

Then check your code for these changes (compared with 1.11.11):

**Data**

- `data.update()` and `data.upsert()` replace every attribute in the payload as a whole. 1.x deep-merged arrays and `json` objects: `tags: ['x', 'y']` updated with `['z']` became `['z', 'y']`, now it is `['z']`. To change one key of a `json` attribute, send the whole object ([`update()`](/sdk/api/data#update)).
- `data.reset({ model })` is staged like the other data methods: the rows are removed by the next `await dcupl.update()`. 1.x cleared the model at once, and the next `update()` brought the rows back.
- `autoGenerateProperties` infers `int`, `float`, `boolean` and `date` (strict ISO 8601) from string values, for example CSV columns. 1.x typed every string column as `string`. Declare the property's type in the model to keep it a `string`.
- `boolean` properties accept `'true'`, `'false'`, `'1'` and `'0'` (any case) and the numbers `1` and `0`, so `'false'` and `'0'` now load as `false` (1.x read every non-empty string as `true`). Any other string, number, object or array is a `WrongDataType` error and stays undefined. CSV columns follow the same rule, except that an empty cell counts as no value, not as `WrongDataType`.
- Rows with the same key in one `data.set()` payload or data file: the last row wins as a whole, with one `NonUniqueKey` error per replaced row. 1.x merged the later rows into the first.
- Keys compare as strings: `data.update([{ key: '1', … }])` changes a row loaded with `key: 1` (1.x ignored the update). A row without a key value (no `key`, or no value for a custom `keyProperty`) is dropped and reported as `MissingKey`; 1.x stored it under the key `'undefined'` (`'null'` for an explicit `null`). See [Rows without a key value](#rows-without-a-key-value).
- An explicit `null` on a reference stays `null` (an `eq null` condition matches it) and is reported as `NullValue` unless the reference sets `quality: { nullable: true }`. 1.x turned it into `undefined`. See [Error Types](/sdk/advanced/quality-validation#error-types).
- A `dcupl.update()` after `init()` that takes the partial path gives the same result as a full rebuild: derived properties and references (also those on resolved references) are recomputed instead of keeping their old values, an update that can change a query reference's result runs as a full update, and the data quality records equal those of an `init()` on the same data ([Partial updates](/sdk/api/data#partial-updates)).
- A list created with `listItemKeys` or `query` stays in sync with the data: after every `dcupl.update()`, partial or full, it serves the current values of its items and drops the removed ones. Items that start matching a seed `query` still join only with `list.update()` or auto-update ([create()](/sdk/api/lists#create)).

**Queries**

- `invert` applies to every operator. 1.x ignored it on `gt`, `gte`, `lt`, `lte`, `size` and `isTruthy`.
- Conditions on `Array<…>` properties match without an index. 1.x returned `[]` until a facet had built the attribute's index.
- Dotted paths through references (`customer.country`) are new in 2.0 ([Deep queries](/sdk/queries/query-deep)); in 1.x such a condition or facet matched nothing.
- A group without `groupKey` added in `add` mode (`addGroup()`, `apply(group)`) is added as its own group. 1.x ignored such a group (one with `groupKey: undefined` written out was merged into the first such group, overwriting its `groupType`).
- A nested group without `groupType` is evaluated as `'and'`. 1.x could return the matches of a later condition when an earlier one matched nothing. A group you pass to `addGroup()`, `setGroup()` or `apply(group)` without `groupType` stays an `'or'` group, as in 1.x.
- The `in` operator is new; in 1.x an `in` condition matched nothing ([Query Operators](/sdk/api/query-operators)).
- A condition whose `operator` is neither built in nor registered as an [operator script](/sdk/guides/scripts) throws an `UnknownOperator` error ([Custom operators](/sdk/api/query-operators#custom-operators)). 1.x matched nothing, silently.
- A sort the SDK can't apply is ignored with a warning: an `order` value other than `'ASC'` or `'DESC'`, an `order` without exactly one entry per attribute, or a sort script key with no registered script. 1.x ignored such a sort silently ([sort scripts](/sdk/guides/scripts#sort--reorder-query-results)).
- `aggregate()` `count` is the number of values the attribute holds (0 when no item has one): each value of an `Array<…>` property or of a multi-valued dotted path counts once, and on a number attribute it is the count `avg` divides by. 1.x returned the number of items in the list, or `undefined` when no item had a numeric value, so `count` on a string or date attribute was always `undefined`. `min` and `max` skip items without a value (1.x could return `null` as the minimum). `sum` and `avg` are unchanged.

**Models**

- `models.set()` replaces the whole definition registered under the key, as `views.set()` already did in 1.x; `models.update()` merges into it ([`set()`](/sdk/api/models#set), [`update()`](/sdk/api/models#update)). In 1.x a second `models.set()` for the same key added its properties to the first one; now the first part is gone. The app loader still merges model files with the same key.
- A `models.set()` or `models.update()` after `init()` takes effect on the next `dcupl.update()`, which rebuilds all data like `init()` (a full update, also when only one row changed). Lists created before the change keep working and serve the new definition.
- `models.get()` and `views.get()` return a copy, and `models.set()` and `views.set()` store one, so changing either object no longer changes the registered definition (1.x handed out the registered definition).
- One app loader per model key: a loader whose `process()` would load a model key that another loader of the instance already loads rejects with `SharedModelKeyError` (exported from `@dcupl/loader`). `dcupl.loaders.remove(key)` removes the models, views and rows that loader loaded on the next `update()` ([Processing](/sdk/api/loader#processing)).

**Events and data quality**

- `dcupl.update()` emits `result_updated` on every list whose result can have changed, and the global `dcupl_partial_update` event when it applies staged data changes or a model definition change (`models.set()`, `models.update()` or `models.remove()` after `init()`, also with no data staged; see [Events](/sdk/api/events)). 1.x emitted neither on a data update. `dcupl_handle_datacontainer` now fires when `update()` rebuilds everything; a partial update emits it, with just the `$DcuplErrorTrackingErrors` and `$DcuplErrorTrackingSummary` containers, whenever the model has recorded data quality errors, whether or not they changed. While no list over those two models exists, they are built at the first read instead of during `init()`/`update()`, and the event for that build fires then ([Quality reports](/sdk/advanced/quality-validation#quality-reports)).
- `dcupl.quality.errors.summary()` is new: error totals per model, attribute and error type.
- A CSV column that the model doesn't declare is reported as an `UnknownColumn` error (1.x dropped it without an error). A column whose values all fail type conversion is reported as `WrongDataType` (1.x: `UndefinedAttribute`).
- A resource that fails to load is recorded as a loader error even with `quality: { enabled: false }`.

**Packages**

- The packages are native ES modules (`"type": "module"`), so `import { Dcupl } from '@dcupl/core'` works in Node ESM. 1.x failed there with `ERR_UNSUPPORTED_DIR_IMPORT`; `require()` still works.
- The browser script builds are gone: `@dcupl/core` and `@dcupl/loader` no longer ship `dist/browser/*.umd.js`. For a `<script>` tag, use the `@dcupl/bundle` package (`dist/dcupl.bundle.min.js`). It defines one global, `initDcupl({ options, appLoaderConfig, processOptions })`, which creates the instance and the loader, fetches the loader configuration, processes it, runs `init()` and resolves `{ dcupl, loader }` (it also sets `window.dcupl`). It doesn't check what `init()` resolved to.
- `dcupl.cacheCtrl` was removed.
- TypeScript 5.4 or later ([Compatibility](/sdk/compatibility#typescript)): the published types use `NoInfer`. `lists.create<T>()` returns a `DcuplList<T>` (1.x: an untyped `DcuplList`), and `list.catalog.query.one()` is typed `| null` (1.x: `| undefined`). See [create()](/sdk/api/lists#create).

**Date values in `distinct`, facets and `suggest`**

- On a `date` attribute, the `distinct` values of `aggregate()`, the facet `value` and `entry.key`, and the `suggest()` values are ISO 8601 strings in UTC, the form `toISOString()` writes (`'2024-03-01T00:00:00.000Z'`). 1.11.11 answered `Date` objects there. Their JSON output is unchanged: `JSON.stringify()` writes a `Date` in the same form.
- The facet `key` of a date is the same ISO string. 1.11.11 answered the date's `toString()` text in the local time zone of the host (`'Fri Mar 01 2024 01:00:00 GMT+0100 (Central European Standard Time)'`), also in JSON. Facet keys you stored or compared, for example in URL state or saved selections, no longer match.
- Rows with the same date share one value: one `distinct` entry with their `count`, one facet with their `size`. 1.x listed each row's date as a value of its own.
- `suggest()` matches the start of the string, so `value: '2024-03'` suggests the dates in March 2024. 1.x answered `[]` for a search term on a date attribute.
- `min` and `max` on a date attribute (`aggregate()`, `pivot()` values, `groupBy()` aggregates) answer neither value in 2.0.0, without an error. From 2.1.0 they are `Date` objects again, as in 1.x.

```typescript group="date-values" tab="1.x"
const distinct = list.catalog.fn.aggregate({ attribute: 'publishedAt', types: ['distinct'] }).distinct ?? [];
distinct[0].value; // a Date at runtime, typed string
(distinct[0].value as unknown as Date).getUTCFullYear(); // 2024
```

```typescript group="date-values" tab="2.0"
const distinct = list.catalog.fn.aggregate({ attribute: 'publishedAt', types: ['distinct'] }).distinct ?? [];
distinct[0].value; // '2024-03-01T00:00:00.000Z'
new Date(distinct[0].value).getUTCFullYear(); // 2024

// The string itself filters on that date, as selecting the facet does
list.catalog.query.addCondition({ attribute: 'publishedAt', operator: 'eq', value: distinct[0].value });
```

What to change: where your code calls `Date` methods on these values or checks `instanceof Date`, convert them with `new Date(value)`. To filter on a value, pass the string unchanged to `eq` or `in`. For `gt`, `gte`, `lt` and `lte`, pass `new Date(value)` or epoch milliseconds: in 2.0.0 an ISO string there matches no row.

**Also new or changed**

- CSV parsing follows RFC 4180: blank lines are skipped (1.x made an empty item of them), a JSON cell keeps the delimiters inside it (1.x cut it at the first one), and `dynamicTyping` types an empty cell as `null`. Check CSV-based data after the upgrade ([DcuplAppLoader](/sdk/api/loader)).
- `dcupl.scripts.get(key)` returns the script's source (1.x returned `undefined`). The `scripting` option can turn scripts off or replace their compiler ([scripts](/sdk/api/dcupl#scripts)).
- New init options: `performance.partialUpdates`, `performance.autoUpdateLists`, `performance.containerBatchSize`, `performance.cache.maxSize`, `quality.maxErrorsPerGroup`, `performance.cache.maxProjectionSize` and `performance.retainStagedData` ([DcuplInitOptions](/sdk/api/dcupl#dcuplinitoptions)). `performance.cache.maxSize` bounds each list's result cache and each model's query indices (default 64 MiB, least recently used entries are dropped; see [Cache Configuration](/sdk/advanced/caching#cache-configuration)).
- Sealed mode is new in 2.0: `performance: { retainStagedData: false }` makes `init()` drop the staged raw rows after a successful build, and `dcupl.seal()` does the same on demand; `dcupl.sealed` reports it. Afterwards every data or model write and a loader's `process()` throw `DcuplSealedError` (exported from `@dcupl/common`), `init()`/`update()` resolve `false`, and every read works as before. The default is unchanged. Measured: 15.6–21.8 % less retained heap after `init()` on a 135,880-row fixture (2026-10-15) and 2–24 % on customer catalogs of 1,000–44,000 rows (2026-10-16); see [seal()](/sdk/api/dcupl#seal).
- `performance.cache.maxProjectionSize`: deep facets and deep filters keep a projected copy of the list per distinct set of dotted attributes, bounded per list (default 256 MiB, least recently used copies are dropped, `Infinity` for no bound), separately from `cache.maxSize` ([Cache configuration](/sdk/advanced/caching#cache-configuration)).
- Error-tracking models: while no list over `$DcuplErrorTrackingErrors` or `$DcuplErrorTrackingSummary` exists, they are built at the first read instead of in every `init()` and `update()`, and errors whose description does not depend on the row share one description string per group. Every read returns the same records. On a 23,754-row customer catalog with about 344,000 recorded errors, building them at the first read and sharing the descriptions took the retained heap after `init()` from 285 MB to 110 MB (2026-10-16); see [Quality reports](/sdk/advanced/quality-validation#quality-reports).
- `@dcupl/common-internal/sandbox` (new in 2.0) exports `createCompartmentCompiler` without loading the package's zod schemas, for hosts that compile scripts in a worker ([The sandbox](/sdk/guides/scripts#the-sandbox)).
- Model typings: `filter` on `int` and `float` properties takes only `boolean` (`FilterNumberArray` is gone), a model's `quality.attributes` no longer takes `validators` (set them per attribute), and `keyProperty` accepts an array for a composite key.

### Rows without a key value

In 2.0 a row whose key has no value is dropped and reported as a `MissingKey` [quality error](/sdk/advanced/quality-validation#error-types), one per dropped row. This holds for both kinds of key:

- The default key: the row has no `key` field, or its `key` is `undefined` or `null`.
- A custom `keyProperty` on the model or the data resource: the row has no value (`undefined` or `null`) for it; for a composite key, for every part.

SDK 1.x stored such a row under the key `'undefined'` (`'null'` for an explicit `null`), and several such rows collapsed into one item per key. With the same data, a model that has such rows holds one item fewer on 2.0, or two fewer if it had both kinds. An empty string is a value: a blank key cell in a CSV file gives the key `''` and the row is kept. A composite key with only some parts missing is kept, with `'undefined'` or `'null'` for those parts ([keyProperty](/sdk/api/models#keyproperty)).

**Find the dropped rows.** The rows are dropped whether quality validation is on or not; `MissingKey` is recorded only while it is on, which is the default. Count the errors with `summary()` (the `errorType` facet of `$DcuplErrorTrackingErrors` leaves `MissingKey` out):

```typescript title="Count rows dropped for a missing key"
const missingKeys = dcupl.quality.errors.summary().filter((group) => group.errorType === 'MissingKey');
// one entry per model and key attribute: group.model, group.attribute, group.totalCount (dropped rows)
```

After `init()`, a keyless row in an `upsert()` that `dcupl.update()` applies on the [partial path](/sdk/api/data#partial-updates) is currently dropped without the error; the next full rebuild reports it.

**What to change.** Give every row a key value before you load it:

- Set `keyProperty` on the model or the data resource to a column that has a value in every row. The error's `attribute` names the key attribute that had none.
- For data without a natural identifier and without a `keyProperty`, set `autoGenerateKey: true`: rows without a `key` get a generated one ([autoGenerateKey](/sdk/api/models#autogeneratekey)). It doesn't fill a custom `keyProperty`.
- Or remove the rows without a key at the source.

## Query API migration (2.0)

The 2.0 query refactor adds clearer, intention-revealing method names next to the old ones. The old methods still work, so existing code keeps running; the new methods say what they do and carry JSDoc examples in your IDE.

### Migration strategy

You don't need to migrate everything at once:

1. Old methods keep working in 2.0 (a few are marked `@deprecated`, see [Deprecation Status](#deprecation-status))
2. Adopt new methods incrementally as you work on code
3. Use new methods for all new code

### Type aliases

The query types have alias names. Both sets are exported from `@dcupl/common` and are the same types (`QueryCondition` = `DcuplQuery`, `QueryConditionGroup` = `DcuplQueryGroup`, `QueryExecutionContext` = `DcuplGlobalQueryOptions`, `ListQueryOptions` = `DcuplListQueryOptions`); neither set is deprecated, so use whichever reads better.

```typescript group="types" tab="Dcupl* names"
import type { DcuplQuery, DcuplQueryGroup } from '@dcupl/common';

const condition: DcuplQuery = { operator: 'eq', attribute: 'status', value: 'active' };
const group: DcuplQueryGroup = { groupType: 'and', queries: [condition] };
```

```typescript group="types" tab="Alias names"
import type { QueryCondition, QueryConditionGroup } from '@dcupl/common';

const condition: QueryCondition = { operator: 'eq', attribute: 'status', value: 'active' };
const group: QueryConditionGroup = { groupType: 'and', queries: [condition] };
```

### Adding query conditions

```typescript group="add" tab="Old API"
// Magic applyQuery with mode options
builder.applyQuery(condition, { mode: 'add' });
builder.applyQuery([condition1, condition2], { mode: 'add' });
builder.applyQuery(group, { mode: 'add' });
```

```typescript group="add" tab="New API"
// Explicit methods that reveal intent
builder.addCondition(condition);
builder.addCondition([condition1, condition2]);
builder.addGroup(group);
```

### Replacing query conditions

```typescript group="set" tab="Old API"
builder.applyQuery(condition, { mode: 'set' });
list.catalog.query.apply(condition, { mode: 'set' });
```

```typescript group="set" tab="New API"
builder.setCondition(condition);
list.catalog.query.setCondition(condition);
```

Both forms replace only the conditions on that condition's attribute; conditions on other attributes are kept. Use `setGroup(group)` to replace a group by its `groupKey`.

### State management

```typescript group="state" tab="Old API"
builder.reset();
builder.removeAllQueries();
builder.getQuery();
```

```typescript group="state" tab="New API"
builder.clear();
builder.clearConditions();
builder.getCurrentQuery();
```

### Executing queries

```typescript group="execute" tab="Old API"
const items = list.catalog.query.execute();
const limited = list.catalog.query.execute({ count: 10 });
const query = list.catalog.query.get();
```

```typescript group="execute" tab="New API"
const items = list.catalog.query.items();
const limited = list.catalog.query.items({ count: 10 });
const first = list.catalog.query.first();
const exists = list.catalog.query.any();
const total = list.catalog.query.count();
const query = list.catalog.query.current();
```

## Complete migration example

```typescript group="example" tab="Before"
import { DcuplQueryBuilder } from '@dcupl/common';

const builder = new DcuplQueryBuilder();
builder.init({ modelKey: 'Product' });

builder.applyQuery({ operator: 'eq', attribute: 'status', value: 'active' }, { mode: 'add' });
builder.applyQuery({ operator: 'gte', attribute: 'price', value: 100 }, { mode: 'add' });
builder.applyQuery(
  {
    groupKey: 'categories',
    groupType: 'or',
    queries: [
      { operator: 'eq', attribute: 'category', value: 'electronics' },
      { operator: 'eq', attribute: 'category', value: 'books' },
    ],
  },
  { mode: 'add' },
);

const query = builder.getQuery();
const attrs = builder.getQueryAttributes(); // 2.0 only (deprecated), not in 1.x
```

```typescript group="example" tab="After"
import { DcuplQueryBuilder } from '@dcupl/common';

const builder = new DcuplQueryBuilder();
builder.init({ modelKey: 'Product' });

builder.addCondition({ operator: 'eq', attribute: 'status', value: 'active' });
builder.addCondition({ operator: 'gte', attribute: 'price', value: 100 });
builder.addGroup({
  groupKey: 'categories',
  groupType: 'or',
  queries: [
    { operator: 'eq', attribute: 'category', value: 'electronics' },
    { operator: 'eq', attribute: 'category', value: 'books' },
  ],
});

const query = builder.getCurrentQuery();
const attrs = builder.getAttributeNames();
const count = builder.getConditionCount();
const isEmpty = builder.isEmpty();
```

## Interactive list migration

```typescript group="interactive" tab="Before"
function applyStatusFilter(status: string) {
  list.catalog.query.apply({ operator: 'eq', attribute: 'status', value: status }, { mode: 'add' });
  return list.catalog.query.execute();
}

function clearFilters() {
  list.catalog.query.reset();
  return list.catalog.query.execute();
}

function getFirstProduct() {
  const results = list.catalog.query.execute({ count: 1 });
  return results[0];
}
```

```typescript group="interactive" tab="After"
function applyStatusFilter(status: string) {
  list.catalog.query.addCondition({ operator: 'eq', attribute: 'status', value: status });
  return list.catalog.query.items();
}

function clearFilters() {
  list.catalog.query.clear();
  return list.catalog.query.items();
}

function getFirstProduct() {
  return list.catalog.query.first();
}

// Additional new capabilities
function hasProducts() {
  return list.catalog.query.any();
}

function getProductCount() {
  return list.catalog.query.count();
}

function hasFilters() {
  return !list.catalog.query.isEmpty();
}
```

## Quick reference tables

### DcuplQueryBuilder methods

| Old Method                            | New Method              | Purpose                     |
| ------------------------------------- | ----------------------- | --------------------------- |
| `getQueryAttributes()` (2.0 only)     | `getAttributeNames()`   | Get attribute name strings  |
| `extractQueryAttributes()` (2.0 only) | `getAttributeDetails()` | Get detailed attribute info |
| `applyQuery(..., {mode:'add'})`       | `addCondition()`        | Add condition(s)            |
| `applyQuery(group, {mode:'add'})`     | `addGroup()`            | Add query group             |
| `applyQuery(..., {mode:'set'})`       | `setCondition()`        | Replace condition(s)        |
| `applyQuery(group, {mode:'set'})`     | `setGroup()`            | Replace a group by key      |
| `reset()`                             | `clear()`               | Full reset                  |
| `removeAllQueries()`                  | `clearConditions()`     | Remove conditions only      |
| `getQuery()`                          | `getCurrentQuery()`     | Get current state           |
| `hasQueryGroup()`                     | `hasGroup()`            | Check group exists          |
| -                                     | `isEmpty()`             | Check if empty              |
| -                                     | `hasCondition()`        | Check condition exists      |
| -                                     | `getConditionCount()`   | Count conditions            |

### CatalogApi methods

| Old Method                   | New Method              | Purpose                                               |
| ---------------------------- | ----------------------- | ----------------------------------------------------- |
| `apply(..., {mode:'add'})`   | `addCondition()`        | Add condition(s)                                      |
| `apply(group, {mode:'add'})` | `addGroup()`            | Add query group                                       |
| `apply(..., {mode:'set'})`   | `setCondition()`        | Replace condition(s)                                  |
| `apply(group, {mode:'set'})` | `setGroup()`            | Replace a group by key                                |
| `remove(condition)`          | `removeCondition()`     | Remove condition(s)                                   |
| `remove({groupKey})`         | `removeGroup(groupKey)` | Remove group                                          |
| `reset()`                    | `clear()`               | Clear all conditions and options (sort, start, count) |
| `get()`                      | `current()`             | Get current query                                     |
| `execute()`                  | `items()`               | Get all results                                       |
| -                            | `first()`               | Get first result                                      |
| -                            | `any()`                 | Check if results exist                                |
| -                            | `count()`               | Count results (respects `start`/`count`)              |
| -                            | `isEmpty()`             | Check if no conditions are set                        |

## Deprecation status

**Current status (2.0):** every old method still works. On `DcuplQueryBuilder`, `applyQuery()`, `removeAllQueries()`, `getQueryAttributes()` and `extractQueryAttributes()` are marked `@deprecated` in JSDoc (`getQueryAttributes()` and `extractQueryAttributes()` were added in 2.0 and never existed in 1.x). The other old names — `reset()`, `getQuery()` and `hasQueryGroup()` on the builder, and `apply()`, `remove()`, `reset()`, `get()` and `execute()` on `list.catalog.query` — are not deprecated.

**Recommendation:** Start using new methods for new code, migrate existing code opportunistically. The [quick reference tables](#quick-reference-tables) list every old name next to its new one.

## Next steps

- [Query Operators](/sdk/api/query-operators) — Complete operator reference
- [Filtering](/sdk/queries/query-filter) — Query fundamentals
- [Deep queries](/sdk/queries/query-deep) — Advanced querying
- [TypeScript Guide](/sdk/guides/typescript) — Type safety patterns
