C3 AI Documentation Home

About the Database Annotation

Use the @db annotation to define how the C3 Agentic AI Platform stores, indexes, and manages the persisted data of an entity Type. The Ann.Db Type defines the properties you can set on the annotation. On this page, you look up the most common fields, copy example annotations for time-series data and for searchable relational data, and verify that an annotation works.

Database annotation fields

Set these fields to determine how the platform persists data for an entity Type. The following table lists each field with what it does, the data store it applies to, and an example. The Default behavior column says what happens if you do not set the field. The table covers the most common fields. For the full list, including the index, order, archive, and shortIdPrefix fields that the examples on this page use, see the Ann.Db Type.

FieldFunctionApplies toDefault behaviorExample
datastoreDetermines where the platform persists data for an entity Type. Set datastore="kv" to store the Type's data in the key-value (KV) store.KV store, SQL storeThe app's default datastore, which is the SQL store@db(datastore="kv") stores the Type's data in the KV store.
domainName of the Db.Domain where the platform persists the Type's data. Your app declares its data domains.KV store, SQL storeNot set@db(domain="primary") stores the data in a data domain named primary that your app declares.
partitionKeyFieldRequired for Types that mix Partitionable, and only for them. Name of the field that defines the partition (a row or set of rows in the KV store) where the platform persists data.KV storeNone. You must set it on a Type that mixes Partitionable.@db(datastore="kv", partitionKeyField="sensorId") partitions data by sensor ID.
partitionKeyBinFor Types that mix Partitionable only. Defines an expression that splits the data for one partition key across multiple KV rows. Requires partitionKeyField.KV storeNo partition key binning@db(partitionKeyField="sensorId", partitionKeyBin="concat(year(timestamp), '-', month(timestamp))") bins the data for each sensor by year and month.
defaultPartitionStrategySets the default strategy for packing a partition's data into buckets, such as the data time range in hot and cold storage and the number of objects per bucket. The platform applies the strategy on a best-effort basis and does not guarantee its exact semantics.KV storeDefault packing strategy appliedSee PartitionBucketStrategy for the configuration options.
persistenceOrderOptional. Order specification for persisting objects within a partition key. Types often inherit it through a mixin. See the Set persistence order section.KV storeNot set@db(persistenceOrder='start') orders rows by the start field, as the platform Observation Type does.
persistDuplicatesDetermines whether the platform keeps duplicate rows within a partition. By default, a duplicate row matches every field except id.KV storefalse. The platform removes duplicate rows.@db(persistDuplicates=true) keeps duplicate rows in the partition.
compactTypeEnables a compact storage format that does not persist the Meta field. Use it for high-velocity Types and for very large Types.KV store, SQL storefalse. The platform persists the Meta field.@db(compactType=true) omits the Meta field to save storage.
uniqueList of unique indexes. Each entry in the list is one index, written as a comma-separated list of fields. A Type that mixes Partitionable supports one unique index entry only.KV store, SQL storeNo unique indexesOn a SQL store Type, @db(unique=['email', 'username']) creates two unique indexes, one on email and one on username. @db(unique=['email, username']) creates one unique index on the combination of both fields.
shortIdGenerates short IDs instead of UUIDs. Each server reserves a range of IDs, so IDs can skip numbers, for example after a server restarts.KV store, SQL storefalse. The platform generates UUIDs.@db(shortId=true, shortIdPrefix="WO-", shortIdReservationRange=1000) generates short IDs that start with WO-.
shortIdReservationRangeThe number of short IDs that each server reserves from the database at a time.KV store, SQL store10000@db(shortId=true, shortIdReservationRange=1000)
columnarStorageDetermines whether the platform stores data in a compressed columnar format, typically for time-series data. You cannot use it with extendable Types.KV storefalse. The platform uses standard row-based storage.@db(columnarStorage=true) enables columnar compression for analytics workloads.

The default SQL store database depends on your environment. A default single-node environment uses H2. Shared environments and single-node environments with a shared database use PostgreSQL. For details, see the Default databases section of Specify a KV Store.

Set persistence order

The persistenceOrder field sorts objects within each partition key. Follow these rules when you set it:

  • Include every field of a unique index in persistenceOrder. If a unique index field is missing from the persistence order of a Partitionable Type, the platform reports an error.
  • Use fields of primitive types when possible, such as a datetime field named timestamp. Primitive fields give better performance than fields whose type is another Type.
  • Use Meta fields in the persistence order with caution. If the persistence order includes a Meta field, the platform disables de-duplication.

Example annotations

Choose @db settings by how your app writes and reads the Type's data. The following examples show two common cases.

High-volume, time-series data

The following annotation serves high-volume, time-series data, such as sensor readings, log entries, or measurement data points:

Type
@db(datastore='cassandra',
    partitionKeyField='parent',
    persistenceOrder='start',
    persistDuplicates=false,
    compactType=true,
    shortId=true,
    shortIdReservationRange=100000)
entity type WindTurbineMeasurement mixes Partitionable<string> {
  // Declare the parent and start fields that the annotation names.
  // ...
}

This annotation defines the following for the WindTurbineMeasurement Type:

  • mixes Partitionable<string>: Makes the Type Partitionable. A Partitionable Type must set partitionKeyField.
  • datastore='cassandra': Specifies the Cassandra datastore. This value requires a store configuration named cassandra. Without a cassandra store configuration, the Type uses the default KV store for your environment. See Specify a KV Store.
  • partitionKeyField='parent': Stores all measurements for a single wind turbine in the same partition, which speeds up queries for that turbine.
  • persistenceOrder='start': Sorts measurements within a partition by time, which speeds up time-range queries.
  • compactType=true and shortId=true: Reduce the overall storage footprint.
  • persistDuplicates=false: Removes duplicate measurements, so the same measurement is stored only once.

Searchable relational data

For Types such as User, PurchaseOrder, or a custom Project Type, data integrity, searchability, and relational queries can matter more than write speed. For example:

Type
@db(unique=['purchaseOrderNumber'], index+=['customer', 'orderDate'])
entity type PurchaseOrder {
  purchaseOrderNumber: !string
  customer: !Customer
  orderDate: !datetime
  // ...
}

This annotation omits the datastore field, so the Type uses the app's default SQL store.

This annotation defines the following for the PurchaseOrder Type:

  • unique=['purchaseOrderNumber']: Creates a unique index on the purchase order number.
  • index+=['customer', 'orderDate']: Adds two indexes, one on customer and one on orderDate, to speed up queries by customer or by order date.

Verify an annotation

After you apply a @db annotation, verify that it has the effect you expect. This section is for developers who want to confirm an annotation in a development environment before they rely on it.

Prerequisites

You need a C3 AI development environment. For more information, see Create an Environment in C3 AI Studio.

Verification workflow

Complete the following workflow to verify an annotation:

  1. Deploy the Type changes to your environment, for example by syncing your package. Schema changes, such as a new unique index or an archive table, take effect after you deploy.
  2. Create test records that the annotation affects.
  3. Run the operation that the annotation changes, such as a fetch, create(), or remove() call.
  4. Check that the result matches your expectation.
  5. Remove the test records when you finish.

Each of the following examples defines a Type with one annotation and gives code to run in the C3 AI Console. The Console shows console.log output in its Logs pane.

Verify a sort order (order)

The order field on a collection field sets the order in which the platform returns the collection's values. The following example sorts a parent's children collection by name in descending order:

Type
/**
 * A child entity that refers to its parent.
 */
entity type MyChild {
  parent: MyParent
}
Type
/**
 * A parent entity whose children collection is sorted by name in descending order.
 */
entity type MyParent {
  /**
   * The child objects whose parent field refers to this object.
   */
  @db(order='descending(name)')
  children: [MyChild](parent)
}

Run the following code in the C3 AI Console:

JavaScript
var parent = MyParent.make({ id: 'parent-1' }).create();
MyChild.make({ id: 'child-a', parent: parent, name: 'Alpha' }).create();
MyChild.make({ id: 'child-z', parent: parent, name: 'Zulu' }).create();

var fetchedParent = MyParent.forId('parent-1').get('children.name');
var firstChildName = fetchedParent.children.first().name;

console.log('First child name:', firstChildName);
// Expected output: First child name: Zulu

This code creates a parent and two children named Alpha and Zulu, then fetches the parent with the names of its children. The first child is Zulu, because the annotation sorts the collection by name in descending order. MyChild does not declare name or id, because every entity Type has both fields.

Verify a unique index (unique)

The unique field lists unique indexes. The following example lists uniqueName as a unique index:

Type
/**
 * An entity with a unique index on the uniqueName field.
 */
@db(unique=['uniqueName'])
entity type MyUniqueType {
  /**
   * A name that is listed as a unique index.
   */
  uniqueName: !string
}

Run the following code in the C3 AI Console:

JavaScript
try {
  MyUniqueType.make({ id: 'A', uniqueName: 'test-name' }).create();
  console.log('First object created successfully.');

  MyUniqueType.make({ id: 'B', uniqueName: 'test-name' }).create(); // This line should fail

  console.error('Test Failed: Unique constraint was not enforced.');

} catch (e) {
  // Inspect e.message to confirm that the error names the duplicate value.
  console.log('Test Passed: Caught expected exception: ' + e.message);
}

The first create() call succeeds. The second create() call uses the same uniqueName value and throws an exception whose message names the duplicate value.

Verify archive on remove (archive)

When you set archive=true, the platform archives instances instead of physically removing them when you call remove(). You can restore an archived instance with unremove(). Do not create a new instance with the ID of an archived instance.

Type
/**
 * An entity whose instances the platform archives on remove.
 */
@db(archive=true)
entity type MyArchivableType {
  /**
   * A data field that the archive preserves with the rest of the object.
   */
  name: string
}

Run the following code in the C3 AI Console:

JavaScript
var obj = MyArchivableType.make({ id: 'archive-test' }).create();
obj.remove();

var afterRemove = MyArchivableType.forId('archive-test');
console.assert(!afterRemove, 'Object should be gone after removal.');

MyArchivableType.make({ id: 'archive-test' }).unremove();
var afterUnremove = MyArchivableType.forId('archive-test');

console.assert(afterUnremove, 'Object should be restored after unremove.');
console.log('Archive and unremove test passed.');

This code creates and removes an instance and confirms that the instance is no longer found. The code then restores the instance with unremove() and confirms that the instance is found again. In the restore call, make builds an in-memory object with the archived ID, and unremove() restores the archived instance with that ID.

See also

Was this page helpful?