agentleFS
srimon12srimon12/duckdb-2-skills/skills/duckdb-sql/SKILL.md

duckdb-sql

Skill1 starsChanged 14 days agoMITView on GitHub

Write and run DuckDB 2.0 SQL. Covers friendly SQL, lambdas, triggers, nested schemas, variables, exact decimals, and the result traps that change answers silently. Use when the user asks for a query, a trigger, a lambda, GROUP BY ALL, PIVOT, MERGE, or a SQL migration.

The file

We split this skill into the rules it gives an agent

11 rules. About 0 are common, 0 are distinctive, and 11 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    When tracking positions, use lambda x, i: for the element and its 1-based index.

    line 27
  2. Very specific:

    When encountering NULL, list_transform preserves NULL elements.

    line 28
  3. Very specific:

    Never write subqueries inside lambdas.

    line 29
  4. Very specific:

    If legacy syntax is required to unblock queries, run SET lambda_syntax = 'enable_single_arrow'.

    line 30
  5. Very specific:

    When querying an unassigned variable, $undefined raises Values were not provided ..., whereas getvariable('undefined') returns NULL.

    line 68
  6. Very specific:

    When creating a nested schema, create the parent schema first (CREATE SCHEMA sales.

    line 80
  7. Very specific:

    If a top-level schema name requires an embedded dot, quote the identifier: CREATE SCHEMA "sales.europe" creates a single top-level schema …

    line 86
  8. Very specific:

    When attaching triggers to temporary tables, reference only temporary objects.

    line 93
  9. Very specific:

    When executing UPDATE, DELETE, or MERGE, follow this verification sequence:

    line 100
  10. Very specific:

    Run the DML statement. If a matching row trigger exists on the table, DuckDB blocks RETURNING (see event rules in references/triggers.md).

    line 104
  11. Very specific:

    When reading execution plans, comparing EXPLAIN against EXPLAIN ANALYZE, profiling operator bottlenecks, or diagnosing memory spills, hand …

    line 161

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

DuckDB 2.0 SQL

Execute statements with duckdb. If a session file from duckdb-session exists, supply it via -init.

Before a large query

To inspect result columns and types without executing a query, run DESCRIBE SELECT .... To compute full column statistics across all rows, run SUMMARIZE <table>. When restricting query output, never run LIMIT without an explicit ORDER BY; unordered limits return nondeterministic rows.

Lambdas

DuckDB disables single-arrow syntax; write lambda x: expr. Higher-order list functions comprise list_transform, list_filter, and list_reduce:

SELECT list_transform([1, 2, 3], lambda x: x + 1);
SELECT list_transform([10, 20], lambda x, i: i);       -- [1, 2], 1-based index

When tracking positions, use lambda x, i: for the element and its 1-based index. When encountering NULL, list_transform preserves NULL elements; list_filter drops elements whose predicate evaluates to NULL. Never write subqueries inside lambdas; subqueries raise a binder error. If legacy syntax is required to unblock queries, run SET lambda_syntax = 'enable_single_arrow'; (removed in 2.1). To filter projection columns by name, write COLUMNS(lambda c: ...); supply an optional second argument lambda c, i: for the 1-based column index.

Division and decimals

ExpressionResult
5 / 22.5, DOUBLE
5 // 22, truncates toward zero
-5 // 2-2, not floor
fdiv(-5, 2)-3.0, floor
5 % 2, -5 % 21, -1; sign follows the dividend
5.5 // 22.75; if either side is decimal or float, // is ordinary division
1 / 0, 0 / 0inf, nan
1 // 0, 1 % 0error
SELECT TRY(1 // 0);  -- returns NULL

To catch division by zero on integer division or modulo without failing, wrap expressions in TRY(...). Avoid SET error_on_division_by_zero = false; this setting is deprecated and emits warnings.

Because avg(DECIMAL) and standard / operations cast operands to DOUBLE, run dedicated decimal functions for exact arithmetic:

  • To sum without precision loss, use sum(x).
  • To compute an exact mean, use decimal_average(x).
  • To divide exact values, use decimal_division(x, y). Cast both operands explicitly to DECIMAL. On 2.0.0 an INTEGER argument is mis-bound and can overflow or return a wrong value: decimal_division(10.00, 4) raises result out of range although 2.5 fits the inferred DECIMAL(15,13), and decimal_division(127, 4.00) returns 0. decimal_division(10.00, 4::DECIMAL(10,0)) returns 2.5. The upstream fix is duckdb/duckdb#28420, labeled for a 2.0 backport; keep the casts until a released binary passes the probe.

Variables

SET VARIABLE x = 7;
SELECT $x, getvariable('x');
SET VARIABLE "my var" = 9;
SELECT $"my var";

To define a session variable, run SET VARIABLE name = value;. Query variables using $name or getvariable('name'). For identifiers containing spaces or special characters, quote with double quotes: SET VARIABLE "my var" = 9; SELECT $"my var";. When querying an unassigned variable, $undefined raises Values were not provided ..., whereas getvariable('undefined') returns NULL. In prepared statements, parameter placeholders ($x) fall back to session variables of the same name when omitted from execution arguments.

Nested schemas

CREATE SCHEMA sales;
CREATE SCHEMA sales.europe;
CREATE TABLE sales.europe.orders (id INTEGER);
SELECT * FROM sales.europe.orders;

When creating a nested schema, create the parent schema first (CREATE SCHEMA sales; CREATE SCHEMA sales.europe;). Nested schemas cannot sit on the search path: USE sales.europe, SET search_path = 'sales.europe', and SET schema = 'sales.europe' all fail. Two-part references resolve from the catalog root, not relative to the current schema; running SELECT * FROM europe.orders after USE sales fails. Running SHOW TABLES inside sales lists only immediate tables, omitting sales.europe.*. To inspect full schema paths, join duckdb_tables() with duckdb_schemas() on schema_oid = oid, exposing schema_name, parent_schema, and parent_schema_oid. To drop a schema tree, specify CASCADE: DROP SCHEMA sales CASCADE. A bare DROP SCHEMA sales fails while child schemas exist. If a top-level schema name requires an embedded dot, quote the identifier: CREATE SCHEMA "sales.europe" creates a single top-level schema where USE "sales.europe" succeeds.

Triggers

A trigger body must consist of exactly one data-modifying statement: INSERT, UPDATE, DELETE, or MERGE, optionally prefixed by WITH. Any SELECT, DDL, COPY, CALL, or PRAGMA body causes a parser error. A body modifying its own target table raises Recursive trigger chains are not yet supported.

On any single table, never mix FOR EACH STATEMENT and FOR EACH ROW triggers. When attaching triggers to temporary tables, reference only temporary objects; modifying main-catalog tables raises a catalog binder error. DuckDB does not support BEFORE ... FOR EACH ROW, AFTER UPDATE ... FOR EACH ROW, row triggers with REFERENCING, WHEN, INSTEAD OF, or CREATE TEMP TRIGGER.

For the trigger support matrix, RETURNING rules, execution order, and metadata queries, read references/triggers.md.

DML proof

When executing UPDATE, DELETE, or MERGE, follow this verification sequence:

  1. BEGIN transaction.
  2. Count matching predicate rows. Before executing MERGE or UPDATE ... FROM, deduplicate the source table: a target row matching multiple source rows causes a runtime error in MERGE and updates from an arbitrary source row in UPDATE ... FROM.
  3. Run the DML statement. If a matching row trigger exists on the table, DuckDB blocks RETURNING (see event rules in references/triggers.md). When RETURNING is blocked, verify modifications via before-and-after row counts or CLI .changes.
  4. COMMIT, then re-count. Treat zero rows affected as an operation result, not proof of success. Never use RETURNING as the sole proof when row triggers exist.

2.0 syntax worth using

FeatureForm
NEAREST joinINNER JOIN p NEAREST 2 BY SIMILARITY array_cosine_similarity(q.e, p.e); APPROX NEAREST for approximate; BY DISTANCE when lower is closer; embeddings must be fixed-size arrays such as DOUBLE[768]
Data-modifying CTEWITH moved AS MATERIALIZED (DELETE FROM staging RETURNING *) INSERT INTO archive SELECT * FROM moved
FETCH FIRSTFETCH FIRST 3 ROWS ONLY; PERCENT and WITH TIES are parser errors
MERGE INTOErrors if one target row matches more than one source row
UPDATE ... FROMMultiple matches update once from an unspecified source row; deduplicate
SECURE VIEWCREATE SECURE VIEW v AS SELECT ...; hides statistics and plan internals
SHOWSHOW threads, SHOW DATABASES, SHOW SCHEMAS; SHOW table is deprecated in favor of DESCRIBE
GROUP BY unnest(col)Groups by list elements
Backtick identifiers`select` is the same as "select"
EXPLAIN (SQL)Renders the optimized plan as executable SQL; cannot combine with ANALYZE or FORMAT
query() / query_table()FROM query('SELECT 42'); literal or prepared string only
Empty STRUCTNULL::STRUCT, struct_pack(), row(); STRUCT() is an error
TUPLE(1, 'a') is a tuple, not a named struct
TIMESTAMPTZ_NSNanosecond precision; TIMESTAMPTZ truncates to microseconds
lttb(x, y, n ORDER BY x)Downsample a series
decimal_division, decimal_averageExact decimal arithmetic
GROUPING_ID() / GROUPING()No arguments default to all GROUP BY columns
ALTER TABLE v RENAME TO v2Also renames a view

COPY ... FROM cannot serve as a CTE body. A COPY ... TO inside a CTE writes the file and returns the number of rows written; Python execute(...).fetchall() returns [(n,)]. On 2.0.0 the CLI does not render that count in JSON, CSV, or box output, so empty CLI output does not mean zero rows. In the CLI, run .changes on to print the count, or add RETURN_STATS or RETURN_FILES to the COPY options to get rendered rows. Prove the write by counting the output file.

Result traps

TrapBehaviorMove
NULL orderingORDER BY x puts NULLs last for both ASC and DESCWrite NULLS FIRST or NULLS LAST explicitly
Integer versus string1 = '1.1' evaluates to true; string is cast to integer and roundedCast explicitly and compare matching data types
Decimal divisionAny / involving DECIMAL returns DOUBLEUse decimal_division(x, y)
Window framesum(x) OVER (ORDER BY x) defaults to RANGE ... and ties share cumulative totalsSpecify ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW for row-by-row accumulation
approx_count_distinctError is data-dependent and can swing several percentUse exact count(DISTINCT x) for user-facing metrics
Empty-group aggregatesum, list, and string_agg return NULL; count returns 0Wrap with coalesce(sum(x), 0)
sum overflowsum(DECIMAL(38,0)) and sum(HUGEINT) can silently overflowCast explicitly to DECIMAL(38,s)
TimestampsCasting DATE to TIMESTAMPTZ evaluates in session time zoneStore instants as TIMESTAMPTZ; set time zone explicitly
CREATE OR REPLACE TABLEDrops existing constraints, primary keys, and indexes without warningUse CREATE TABLE IF NOT EXISTS plus INSERT or MERGE
PIVOT ... FILTERAccepted by parser and silently ignoredCompute filtered aggregates in a CTE prior to pivoting
Simplified UNPIVOTSilently drops rows with NULL valuesUse standard UNPIVOT (... INCLUDE NULLS)
DISTINCT ON without ORDER BYSelects an arbitrary row for each groupAdd ORDER BY starting with the DISTINCT ON key columns
USING SAMPLEWritten after WHERE but evaluated before the filterPlace filter in a subquery prior to sampling
NEAREST ... AND <filter>Filter joins the distance ranking key, returning non-nearest rowsFilter candidate rows in a CTE before joining
1 IN (0, NULL)Scalar returns NULL; list 1 IN [0, NULL] returns falseUse IS NOT DISTINCT FROM or coalesce
~, !~2.0 matches substrings; 1.x matched full stringUse regexp_full_match for 1.x semantics; see duckdb-migrate

For friendly-SQL patterns and edge-case examples behind these traps, read references/friendly-sql.md.

Parser

The PEG parser is active by default; syntax errors report LINE n: with an exact position caret. If a query that parsed in 1.5 fails under 2.0, run SET enable_legacy_parser = true; to autoload the extension and switch parsers for the session. Note that the legacy parser rejects simplified FROM ... NEAREST syntax; explicit INNER JOIN ... NEAREST still parses. Treat the legacy parser strictly as a temporary syntax fallback to unblock migrations, not a semantic time machine. File an upstream issue, unblock immediately, and remove the setting from production SQL.

Before optimizing

When reading execution plans, comparing EXPLAIN against EXPLAIN ANALYZE, profiling operator bottlenecks, or diagnosing memory spills, hand off to duckdb-profile. EXPLAIN (SQL) belongs here because it decompiles optimized query representations back into executable SQL rather than reporting runtime profile metrics.

Verify

SELECT list_transform([1, 2, 3], lambda x: x + 1) AS lambda,
       TRY(1 // 0) AS safe_divide,
       decimal_division(10.00, 4.00) AS exact;

Expect [2, 3, 4], NULL, and 2.500000.

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.