# Rules
> Name a check, the trading hours or the dedup choice of a table once, and KDB applies it to every row written - saying which rule refused a row and why.
- Category: Tables
- Keywords: rule, rules, CREATE RULE, ALTER RULE, DROP RULE, SHOW RULES, kdb\_rules, price guard, value range, session hours, trading hours, ON FAIL SKIP, ON FAIL REJECT, dedup\_keep, keep first, check rule
- See also: [CREATE TABLE](create-table.md), [Trading calendars](calendars.md), [Partitions, SORT BY and DEDUP KEY](partitions-and-dedup.md), [Time and time zones](time-and-time-zones.md)
A **rule** is a named check or setting of one table. It applies to every row written to the table - `INSERT`, `UPDATE`, `COPY`, `/imp`, `/write` and line protocol - and a row it refuses says **which rule and why**. Rules are listed by `SHOW RULES`, switched off and on by `ALTER RULE`, and saved with the table.
| Kind | Example | What it does |
|---|---|---|
| check | `CHECK (close > 0)` | every new or changed row must not make the condition false |
| session | `SESSION '11:00' TO '15:00' TIME ZONE 'Asia/Kathmandu' CALENDAR nepse` | the row's time must be inside the hours, on a trading day of the calendar |
| setting | `SET dedup_keep = 'first'` | which of two rows with the same DEDUP KEY stays |
## CREATE RULE
Syntax:
```sql
CREATE RULE [IF NOT EXISTS] name ON table
CHECK (condition)
| SESSION 'HH:MM' TO 'HH:MM' [TIME ZONE 'zone'] [CALENDAR calendar]
| SET dedup_keep = 'first' | 'last'
[ON FAIL REJECT | SKIP] [MESSAGE 'text']
```
| Part | Meaning |
|---|---|
| `CHECK (condition)` | a condition on the values of one row, as a `CHECK` constraint. A condition that is `NULL` passes (standard SQL): write `close IS NOT NULL AND close > 0` to refuse a missing price |
| `SESSION ... TO ...` | the row's time, on the clock of `TIME ZONE` (UTC when not given), from the first time to the second (both included). With `CALENDAR`, only on its trading days ([calendars](calendars.md)). The table needs a time column |
| `ON FAIL REJECT` | the default: the statement fails, and nothing of it is written (PostgreSQL code `23514`) |
| `ON FAIL SKIP` | the row is left out and the statement goes on: the answer says `..., 1 left out by rules (ON FAIL SKIP)`. An `UPDATE` that would break the rule is refused instead (a row already there is never left out) |
| `MESSAGE 'text'` | your words in the refusal, before the reason |
| `SET dedup_keep = 'first'` | a row whose DEDUP KEY the table holds already is left out, whatever the write asked (also `INSERT OR REPLACE` and `/write`). `'last'` is the default: the new row replaces the old one. The table needs a `DEDUP KEY` |
- **Rows already there:** a new rule checks rows written from then on. Find older rows that break it with `SELECT ... WHERE NOT (condition)`.
- **Names:** a rule name is unique in the database. `DROP TABLE` drops the table's rules with it.
- **Role:** `ddl`, like `CREATE TABLE`.
- **Not yet:** `SET null_compare` (comparisons with `NULL` as QuestDB does them) is refused with that reason; comparisons follow standard SQL.
**Example: A price guard: the refused row names its rule** (shows an error)
Setup:
```sql
CREATE TABLE ohlc (time TIMESTAMP, symbol SYMBOL, open DOUBLE, close DOUBLE) TIMESERIES(time) PARTITION BY DAY;
CREATE RULE price_guard ON ohlc CHECK (open > 0 AND close > 0) MESSAGE 'prices must be above 0';
```
```sql
INSERT INTO ohlc VALUES ('2026-10-01T05:30:00Z', 'NABIL', 500, 0);
```
Result:
Error: `rule 'price_guard' of table 'ohlc' refused the row: prices must be above 0 (open > 0 AND close > 0 is false for the row open='500', close='0')`
**Example: ON FAIL SKIP leaves the bad row out and keeps the others**
Setup:
```sql
CREATE TABLE ticks (time TIMESTAMP, symbol SYMBOL, price DOUBLE, qty DOUBLE) TIMESERIES(time) PARTITION BY DAY;
CREATE RULE no_negative_qty ON ticks CHECK (qty >= 0) ON FAIL SKIP;
```
```sql
INSERT INTO ticks VALUES ('2026-10-01T05:30:00Z', 'NABIL', 500, 10), ('2026-10-01T05:31:00Z', 'NABIL', 501, -3),
('2026-10-01T05:32:00Z', 'NABIL', 502, 7);
```
Result:
```text
2 row(s) inserted, 1 left out by rules (ON FAIL SKIP)
```
**Example: Trading hours of NEPSE: 11:00 to 15:00 in Kathmandu, on its trading days** (shows an error)
Setup:
```sql
CREATE CALENDAR nepse_days (TIME ZONE 'Asia/Kathmandu', ERA FROM '2022-01-01' DAYS sun-thu);
CREATE TABLE trades (time TIMESTAMP, symbol SYMBOL, price DOUBLE) TIMESERIES(time) PARTITION BY DAY;
CREATE RULE nepse_hours ON trades SESSION '11:00' TO '15:00' TIME ZONE 'Asia/Kathmandu' CALENDAR nepse_days;
```
```sql
INSERT INTO trades VALUES ('2026-10-01T10:30:00Z', 'NABIL', 500);
```
Result:
Error: `rule 'nepse_hours' of table 'trades' refused the row: its time 2026-10-01T16:15:00 Asia/Kathmandu is outside the session 11:00 to 15:00`
## ALTER RULE
Syntax:
```sql
ALTER RULE [IF EXISTS] name ENABLE | DISABLE | RENAME TO new_name
```
A disabled rule stays, and checks nothing until `ENABLE`.
## DROP RULE
Syntax:
```sql
DROP RULE [IF EXISTS] name [, name ...]
```
## See the rules: SHOW RULES
`SHOW RULES` is `SELECT * FROM kdb_rules`: one row per rule.
| Column | Meaning |
|---|---|
| `rule`, `table_name` | the rule and its table |
| `kind` | `check`, `session` or `set` |
| `definition` | what follows `ON table` in its `CREATE RULE` |
| `on_fail` | `reject` or `skip` (empty for a setting) |
| `message`, `enabled` | its message, and whether it is on |
**Example: All rules of the database**
Setup:
```sql
CREATE TABLE quotes (time TIMESTAMP, symbol SYMBOL, bid DOUBLE, ask DOUBLE) TIMESERIES(time) PARTITION BY DAY;
CREATE RULE spread_ok ON quotes CHECK (ask >= bid) MESSAGE 'ask below bid';
```
```sql
SHOW RULES;
```
Result:
| rule (STRING) | table_name (STRING) | kind (STRING) | definition (STRING) | on_fail (STRING) | message (STRING) | enabled (BOOL) |
|---|---|---|---|---|---|---|
| spread_ok | quotes | check | CHECK (ask >= bid) | reject | ask below bid | true |
(1 row)