Sitting Duck 🦆

July 26, 2026 · View on GitHub

DuckDB Community Extension Documentation

Sitting Duck 🦆

Sitting Duck is a DuckDB extension that makes Abstract Syntax Trees (ASTs) from source code files quack like data - enabling powerful SQL-based analysis across multiple programming languages.

CLI Quick Start

$ duckdb -ascii -noheader -s "SELECT peek FROM read_ast('src/**/*.cpp', peek='full') WHERE is_function_definition(semantic_type) AND name='FindChildByTypeNode';"

TSNode LanguageAdapter::FindChildByTypeNode(TSNode node, const string &child_type) const {
    uint32_t child_count = ts_node_child_count(node);
    for (uint32_t i = 0; i < child_count; i++) {
        TSNode child = ts_node_child(node, i);
        const char* type = ts_node_type(child);
        if (child_type == type) {
            return child;
        }
    }
    return {0}; // Return null node
}

real    0m0.112s
user    0m0.303s
sys     0m0.103s

Why "Sitting Duck"?

The name reflects the project's philosophy and technology stack:

  • Sitting: A nod to Tree-sitter, our parsing engine - your code sits in trees waiting for analysis
  • Duck: Everything quacks like data in DuckDB - including your source code!
  • Target: Your codebase becomes a sitting duck for powerful SQL-based analysis

What Makes It Special

Traditional code analysis tools force you to learn their APIs and query languages. Sitting Duck lets you use the most powerful data analysis language ever created - SQL - to explore your codebase.

Code is data. Data wants to be queried. DuckDB makes querying a joy. Therefore: Analyzing code should be joyful. 🦆

What It Does

Sitting Duck lets you analyze source code using SQL queries with language-specific semantic understanding. Parse your codebase once, then query it like any other database:

-- Load the extension
LOAD sitting_duck;

-- Find all functions with their signatures (native context)
SELECT name, signature_type, parameters, start_line, end_line
FROM read_ast('my_script.py', 'python', context := 'native')
WHERE type = 'function_definition';

-- Analyze Java method return types
SELECT name, signature_type as return_type, start_line
FROM read_ast('MyClass.java', 'java', context := 'native')
WHERE type = 'method_declaration';

-- Count different node types
SELECT type, COUNT(*) 
FROM read_ast('my_script.py') 
GROUP BY type 
ORDER BY COUNT(*) DESC;

Architecture

Sitting Duck transforms your source code into queriable data structures:

  1. Tree-sitter parsing - Robust, error-recovering parsers for 27 languages
  2. Native semantic extraction - Language-specific semantic analysis with type information
  3. Multiple context levels - From basic parsing to full semantic understanding
  4. SQL interface - Rich table functions with DuckDB-consistent design
  5. Memory-safe processing - Comprehensive error handling, backed by memory-safety stress tests
  6. Streaming design - Efficient processing of large codebases

Supported Languages

Currently supports 27 languages via Tree-sitter parsers, all with universal semantic type classification. Native extraction depth (names, signatures, parameters, modifiers) varies by language — see the per-language docs below for quality ratings:

CategoryLanguages
WebJavaScript, TypeScript, HTML, CSS
SystemsC, C++, Go, Rust, Zig
ScriptingPython, Ruby, PHP, Lua, R, Bash
EnterpriseJava, C#, Kotlin, Swift, Dart
Data/QuerySQL, DuckDB, GraphQL, JSON
ConfigHCL (Terraform), TOML
DocsMarkdown

All languages include semantic type extraction with refinements (Function::LAMBDA, Variable::MUTABLE, etc.).

Language Documentation:

Each doc includes extraction quality ratings, implementation notes, and known limitations.

Installation

# Clone with submodules (required for Tree-Sitter and Tree-Sitter grammars)
git clone --recursive https://github.com/teaguesterling/sitting_duck.git
cd sitting_duck

# Build the extension.
# NOTE: a bare `make` builds single-threaded. To use all cores, set the cmake
# parallel level (the build invokes `cmake --build` without -j):
CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) make
# (Alternatively `GEN=ninja make` if ninja is installed — ninja parallelizes by default.)
# vcpkg is NOT required: vcpkg.json declares no dependencies.

# Test it works
./build/release/duckdb -c "LOAD './build/release/extension/sitting_duck/sitting_duck.duckdb_extension'; SELECT COUNT(*) FROM read_ast('README.md');"

Basic Usage

Parse a Single File

-- See all nodes in a file
SELECT * FROM read_ast('example.py') LIMIT 10;

-- Find function definitions
SELECT name, start_line, end_line
FROM read_ast('example.py')
WHERE type = 'function_definition';

-- Show tree structure
SELECT 
    repeat('  ', depth) || type as indented_type,
    name,
    start_line
FROM read_ast('example.py')
ORDER BY node_id
LIMIT 20;

Parse Multiple Files

-- Single glob pattern
SELECT file_path, COUNT(*) as node_count
FROM read_ast('src/**/*.py')  
GROUP BY file_path
ORDER BY node_count DESC;

-- Array of specific files
SELECT file_path, language, COUNT(*) as nodes
FROM read_ast([
    'main.py',
    'utils.py', 
    'config.js'
])
GROUP BY file_path, language
ORDER BY nodes DESC;

-- Process files with batch processing
SELECT file_path, COUNT(*) as nodes
FROM read_ast([
    'src/module1.py',
    'src/module2.py'
], batch_size := 2)
GROUP BY file_path;

-- Find functions in specific files
SELECT file_path, name, start_line
FROM read_ast(['main.py', 'utils.py'])
WHERE type = 'function_definition'
ORDER BY file_path, start_line;

DuckDB-Consistent Interface

Sitting Duck follows DuckDB conventions, making it feel native to DuckDB users:

Function Overloads (like read_csv, read_parquet)

-- Single file (VARCHAR)
SELECT * FROM read_ast('main.py');

-- Array of files (LIST(VARCHAR))
SELECT * FROM read_ast(['main.py', 'utils.py']);

-- With explicit language
SELECT * FROM read_ast('script.py', 'python');
SELECT * FROM read_ast(['file1.py', 'file2.py'], 'python');

-- With native context extraction
SELECT * FROM read_ast('main.py', 'python', context := 'native');

Named Parameters

-- Error handling (like other DuckDB file functions)
SELECT * FROM read_ast(['file1.py', 'file2.py'], ignore_errors := true);

-- Context extraction levels
SELECT * FROM read_ast('main.py', context := 'native');        -- Full semantic analysis
SELECT * FROM read_ast('main.py', context := 'normalized');    -- Cross-language normalization
SELECT * FROM read_ast('main.py', context := 'node_types_only'); -- Basic types only

-- Source and structure control
SELECT * FROM read_ast('main.py', source := 'full', structure := 'full');

-- Performance tuning
SELECT * FROM read_ast('main.py', peek := 50);                 -- Limit peek size
SELECT * FROM read_ast('main.py', peek := 'smart');            -- Smart peek mode

-- Batch processing
SELECT * FROM read_ast(['file1.py', 'file2.py'], batch_size := 2);

Consistent Behavior

  • Parameter validation: Comprehensive validation with clear error messages
  • Error handling: Graceful handling with ignore_errors parameter
  • Memory safety: Robust multi-file processing with corruption prevention
  • Native extraction: Language-specific semantic analysis (Go, Java, C++, Python, JavaScript, etc.)
  • Batch processing: Configurable batch sizes for performance optimization

Table Schema

The read_ast() function returns a table with one row per AST node. Column set depends on extraction parameters:

ColumnTypeDescription
node_idBIGINTUnique node identifier
typeVARCHARTree-sitter AST node type (e.g., 'function_definition')
semantic_typeSEMANTIC_TYPEUniversal semantic category
flagsUTINYINTNode property flags (use has_body(), is_declaration_only())
nameVARCHARExtracted identifier name
signature_typeVARCHARType/return type information
parametersSTRUCT[]Function parameters with names and types
modifiersVARCHAR[]Access modifiers and keywords
annotationsVARCHARDecorator/annotation text
qualified_nameLIST<STRUCT>Scope-based definition path as segment list ({semantic_type, name, index}), unique within a file
scopeSTRUCTScope info: {current, function, class, module, stack}. scope.function answers "what function is this inside?" as a single field read.
file_pathVARCHARSource file path
languageVARCHARDetected programming language
start_lineUINTEGERStarting line number (1-based)
end_lineUINTEGEREnding line number (1-based)
start_columnUINTEGERStarting column (only with source := 'full')
end_columnUINTEGEREnding column (only with source := 'full')
parent_idBIGINTParent node ID (NULL for root)
depthUINTEGERTree depth (0 for root)
sibling_indexUINTEGERPosition among siblings (0-based)
children_countUINTEGERNumber of direct children
descendant_countUINTEGERTotal descendants (useful for complexity)
peekVARCHARSource code snippet for this node

See Output Schema for detailed column documentation.

Context Extraction Levels

The extension supports multiple context extraction levels via the context parameter:

  • 'none': Minimal processing, fastest performance
  • 'node_types_only': Basic AST node types only
  • 'normalized': Cross-language semantic normalization
  • 'native': Full language-specific semantic analysis (recommended)

semantic_type always contains the universal semantic category (the same taxonomy in every mode). Native context mode additionally populates the extraction fields — name, signature_type, parameters, modifiers — with language-specific detail:

  • Go: return types, method receivers, parameter names
  • Java: return types, parameters, access modifiers
  • C++: return types, parameters, class/struct kinds
  • Python: parameters, decorators, class kinds
  • JavaScript: parameters, arrow functions, const/let/var

Quick Start

# Install the extension
make

# Use SQL directly
./build/release/duckdb -c "LOAD sitting_duck; SELECT * FROM read_ast('main.py') LIMIT 10;"
# Technically THIS build doesn't need to LOAD sitting_duck.

# Or use the CLI tool (if available)
./tools/ast-cli/ast funcs "**/*.py" "test*"    # Find test functions

Real Examples

Find Complex Functions

-- Functions with >100 AST nodes (complexity indicator)
SELECT name, file_path, descendant_count as complexity
FROM read_ast('**/*.py')
WHERE type = 'function_definition' 
  AND descendant_count > 100
ORDER BY complexity DESC;

Analyze Import Patterns

-- Most imported modules
SELECT 
    regexp_extract(peek, 'from (\w+)', 1) as module,
    count(*) as usage_count
FROM read_ast('**/*.py')
WHERE type = 'import_from_statement'
GROUP BY module
ORDER BY usage_count DESC;

Class Hierarchy

-- Find all classes and their methods
SELECT
    c.name as class_name,
    c.file_path,
    m.name as method_name,
    m.start_line
FROM read_ast('**/*.py') c
JOIN read_ast('**/*.py') m ON m.parent_id = c.node_id
WHERE c.type = 'class_definition'
  AND m.type = 'function_definition'
ORDER BY c.name, m.start_line;

Code Search Examples

Find a Function Definition by Name

# Find a specific function and display its full source
duckdb -ascii -noheader -s "
SELECT peek
FROM read_ast('src/**/*.cpp', context := 'native', peek := 'full')
WHERE is_function_definition(semantic_type)
  AND name = 'FindChildByTypeNode';"

Find All Calls to a Function

# Find all places where ts_node_type() is called
duckdb -csv -noheader -s "
SELECT file_path, start_line, peek
FROM read_ast('src/**/*.cpp', context := 'native', peek := 60)
WHERE is_function_call(semantic_type)
  AND name = 'ts_node_type';"

Find Method Calls (Object.method pattern)

For method calls like obj.method(), the name field is empty but signature_type contains the full call expression:

-- Find all calls to .empty() method (any object)
SELECT file_path, start_line, signature_type, peek
FROM read_ast('src/**/*.cpp', context := 'native', peek := 60)
WHERE is_function_call(semantic_type)
  AND (
    signature_type LIKE '%.empty'   -- dot notation: obj.empty()
    OR signature_type LIKE '%->empty' -- arrow notation: ptr->empty()
  );

Find a Method Within a Class (Python)

-- Find MyClass.my_method definition
WITH class_blocks AS (
    SELECT c.name as class_name, b.node_id as block_id
    FROM read_ast('myfile.py', context := 'native') c
    JOIN read_ast('myfile.py', context := 'native') b
        ON b.parent_id = c.node_id AND b.type = 'block'
    WHERE c.type = 'class_definition'
)
SELECT
    cb.class_name || '.' || m.name as qualified_name,
    m.signature_type as return_type,
    m.parameters,
    m.start_line,
    m.peek
FROM class_blocks cb
JOIN read_ast('myfile.py', context := 'native', peek := 80) m
    ON m.parent_id = cb.block_id
WHERE m.type = 'function_definition'
  AND cb.class_name = 'MyClass'
  AND m.name = 'my_method';

Cross-Language Function Comparison

-- Compare how factorial is implemented across languages
SELECT
    language,
    name,
    signature_type as return_type,
    parameters,
    modifiers
FROM read_ast([
    'examples/factorial.py',
    'examples/factorial.rs',
    'examples/factorial.go',
    'examples/factorial.java'
], context := 'native', ignore_errors := true)
WHERE is_function_definition(semantic_type)
  AND name LIKE '%factorial%'
ORDER BY language;

Native Extraction Fields

When using context := 'native', the following fields provide semantic information:

FieldDescriptionExample Values
nameIdentifier namefactorial, MyClass, count
signature_typeType info (return type, class kind)int, void, class, trait
parametersParameter names (functions)['n', 'acc'], ['self', 'x']
modifiersAccess/declaration modifiers['public', 'static'], ['abstract']

Extraction by Semantic Type

Semantic Typenamesignature_typeparametersmodifiers
DEFINITION_FUNCTIONfunction namereturn typeparam namesaccess modifiers
DEFINITION_CLASSclass nameclass/interface/trait[]inheritance info
COMPUTATION_CALLfunc name OR empty*full call expr[][]
DEFINITION_VARIABLEvariable namevariable type[]const/let/var

*For method calls like obj.method(), name is empty but signature_type contains obj.method

Cross-Language Support

LanguageFunctionsClassesMethod CallsVariables
Javareturn type, params, modifiersclass/interface, inheritancefull signaturetype
Rustreturn type, paramstrait/struct/enumfull signaturetype, mut
Goreturn type, paramsstruct/interfacepackage.functype
C++return type, paramslimitedfull signaturetype
Pythonparams onlyclass kind, inheritancefull signature-
JavaScriptparamsclassfull signatureconst/let/var

Performance Notes

  • Streaming: Parses files one-by-one, so you see results immediately
  • Memory efficient: the streaming read_ast path holds only one file's AST in memory at a time. (The internal parallel batch path buffers each file's parsed result until the batch completes, so peak memory there scales with batch size.)
  • Glob patterns: Use **/*.ext for recursive directory searches
  • Peek modes: Control how much source text to extract (affects performance)
-- Fastest: no source text
SELECT COUNT(*) FROM read_ast('**/*.py', peek := 'none');

-- Balanced: adaptive snippets (the default)
SELECT COUNT(*) FROM read_ast('**/*.py', peek := 'smart');

-- Fixed-size snippets: at most N characters per node
SELECT COUNT(*) FROM read_ast('**/*.py', peek := 60);

-- Complete: full source text (slower)
SELECT COUNT(*) FROM read_ast('**/*.py', peek := 'full');

peek_size and peek_mode are accepted as legacy aliases for backward compatibility; new code should use peek.

Utility Functions

Semantic Type Predicates

Cleaner filtering with predicate macros:

-- Find all function definitions
SELECT * FROM read_ast('file.py')
WHERE is_function_definition(semantic_type);

-- Find all class definitions
SELECT * FROM read_ast('file.py')
WHERE is_class_definition(semantic_type);

-- Find all function calls
SELECT * FROM read_ast('file.py')
WHERE is_function_call(semantic_type);

Available predicates: is_function_definition, is_class_definition, is_variable_definition, is_function_call, is_member_access, is_string_literal, is_number_literal, is_conditional, is_loop, is_jump, is_assignment, and more.

Source Extraction

Extract source code from files:

-- Extract source code for a function
SELECT
    name,
    ast_get_source(file_path, start_line, end_line) AS source
FROM read_ast('file.py')
WHERE is_function_definition(semantic_type);

-- Get a single line
SELECT ast_get_source_line('file.py', 42);

-- Get source with line numbers
SELECT ast_get_source_numbered('file.py', 10, 25);

For full-featured line reading (globs, line specs, context windows, lateral joins), see the duckdb_read_lines extension.

Advanced Features

Semantic Types for Cross-Language Analysis

The extension includes a universal semantic taxonomy that works across all languages. Use convenience functions for readable queries:

-- Find all function definitions across languages with native context
SELECT file_path, name, language, semantic_type
FROM read_ast('main.py', 'python', context := 'native')
WHERE type = 'function_definition';

-- Compare Java and C++ method signatures
(
  SELECT 'java' as lang, name, signature_type as return_type
  FROM read_ast('MyClass.java', 'java', context := 'native')
  WHERE type = 'method_declaration'
)
UNION ALL
(
  SELECT 'cpp' as lang, name, signature_type as return_type
  FROM read_ast('MyClass.cpp', 'cpp', context := 'native')
  WHERE type = 'function_definition'
);

-- Analyze function complexity in native context
SELECT 
    name,
    semantic_type,
    descendant_count as complexity
FROM read_ast('complex_file.py', 'python', context := 'native')
WHERE type = 'function_definition'
ORDER BY complexity DESC;

Native Context Features:

  • Language-specific extraction: Method signatures, return types, and parameters via signature_type, parameters, modifiers
  • Cross-language compatibility: Compare similar constructs across languages
  • Enhanced analysis: More detailed semantic information than normalized context
  • Extensively tested: Includes memory-safety stress tests and multi-execution stability tests

Use context := 'native' for the most detailed analysis. See language-specific tests for examples.

Native Context Extraction

The native context mode provides language-specific semantic analysis:

-- Get Java method signatures with return types
SELECT 
    name as method_name,
    signature_type as return_type,
    start_line
FROM read_ast('MyClass.java', 'java', context := 'native')
WHERE type = 'method_declaration';

-- Find Go function signatures
SELECT 
    name as function_name,
    signature_type as return_type,
    parameters,
    file_path
FROM read_ast('*.go', 'go', context := 'native')
WHERE type = 'function_declaration';

-- Analyze C++ class hierarchies
SELECT 
    name as class_name,
    signature_type as class_kind,
    descendant_count as complexity
FROM read_ast('*.cpp', 'cpp', context := 'native')
WHERE type = 'class_specifier'
ORDER BY complexity DESC;

Pattern Matching

Find code structures using pattern-by-example matching with wildcards:

-- Load pattern matching macros
.read src/sql_macros/pattern_matching.sql

-- Create AST table
CREATE TABLE code AS SELECT * FROM read_ast('src/**/*.py');

-- Find all eval() calls and capture their arguments
SELECT file_path, start_line, captures['X'].peek as argument
FROM ast_match('code', 'eval(__X__)', 'python');

-- Find nested calls like len(str(__X__))
SELECT * FROM ast_match('code', 'len(str(__X__))', 'python');

-- Use variadic wildcards for flexible matching
-- %__BODY<*>__% matches 0+ siblings at that level
SELECT captures['F'].name as func_name
FROM ast_match('code',
    'def __F__(__):
        %__BODY<*>__%
        return __Y__',
    'python');

Wildcard syntax:

  • __X__ - Named wildcard, captures as 'X'
  • __ - Anonymous wildcard, matches but doesn't capture
  • %__X<*>__% - Named variadic: matches 0+ siblings
  • %__<*>__% - Anonymous variadic: matches 0+ siblings (no capture)

See Pattern Matching Guide for full documentation.

Patching Source with AST Anchors

ast_patch applies edits-as-data — plain rows anchored at AST node positions — to source files, returning the patched text. Combined with ast_node_edit (build an edit from a matched node) and ast_replace (selector → replacement in one call), this is codemods in SQL: the first step of the patch → unparse → rewrite plan in the v2 architecture RFC (docs/planning/v2-architecture.md on the docs/v2-architecture-rfc branch).

-- One-shot: rename a function everywhere its identifier appears
SELECT file_path, patched_source
FROM ast_replace('src/main.py', 'identifier[name=old_fn]', 'new_fn');

-- Or explicitly: match nodes, build edits, apply
CREATE TABLE edits AS
SELECT unnest(ast_node_edit(r, 'replace', 'new_fn'))
FROM read_ast('src/**/*.py', source := 'full') r
WHERE r.type = 'identifier' AND r.name = 'old_fn';

SELECT file_path, patched_source FROM ast_patch('edits', 'src/**/*.py');

The edit flow:

  1. Parse with source := 'full' (location columns start_column/end_column only exist at this level; positions are 1-indexed byte offsets, end_column exclusive).
  2. Match the nodes to change (any WHERE, selector, or pattern you like) and build edit rows: (file_path, start_line, start_column, end_line, end_column, edit_kind, new_text) with edit_kind one of replace, delete, insert_before, insert_after. ast_node_edit(node, kind, new_text) builds one from an AST row — expand it with unnest(...).
  3. Apply: ast_patch(edits_table_name, files_glob) re-reads each file, validates, and returns (file_path, patched_source). The files glob is required because DuckDB's read_text cannot take per-row paths — pass the same glob you parsed.

Parse and patch immediately. Positions anchor to the file content that was parsed; ast_patch re-reads files at application time, so edits held across file changes go stale. Any position that no longer fits the current content errors (a cheap staleness guard — it cannot catch same-shape drift, so treat parse → build edits → patch as one motion). Overlapping edits, unreadable files, unknown kinds, and NULL anchors all error loudly — no partial output.

Why files are re-read (and parse_ast output can't be patched): no extraction configuration retains per-node source text — source := controls location columns only, and peek is presentation, never a correctness substrate. Exact source therefore only exists in the file itself; edits anchored to in-memory parse_ast() results (file_path = '<inline>') error with a clear message. Source-text retention is planned v2 engine work (see the RFC's substrate-gap note).

Writing back: ast_patch is pure — it never writes. To write a patched file, use COPY:

COPY (SELECT patched_source FROM ast_patch('edits', 'src/main.py'))
TO 'src/main.py' (FORMAT csv, QUOTE '', ESCAPE '', HEADER false);

ast_replace(source, selector, new_text, language := NULL) matches via the CSS selector engine (ast_select) and replaces each match's exact byte range. Replacement is literal for now — capture interpolation needs per-capture exact source, which is blocked on the same v2 source-retention work. A selector that matches both a node and its descendant produces overlapping edits and errors: refine the selector.

Rendering Code as Documents (duck_blocks)

ast_to_blocks converts parsed ASTs into duck_blocks — the document-element STRUCT spec shared by the markdown / webbed / duck_block_utils extensions — so code structure renders as a readable document: definitions become headings (definition nesting depth → heading level), definition bodies become code blocks, and each file opens with a YAML metadata block.

duck_blocks is a spec, not a dependency: ast_to_blocks emits conforming STRUCTs with nothing loaded. If the duck_block_utils extension is available, rendering straight to the terminal is one query:

LOAD sitting_duck;
LOAD duck_block_utils;
PRAGMA duck_block_render;

-- One query: parse a file, render its structure as an ANSI document
SELECT db_render_blocks(blocks) FROM ast_to_blocks_list('src/main.py');

The row-shaped macro composes with the standard list(... ORDER BY ...) idiom:

-- One duck_block per row (columns: file_path, element_order, block)
SELECT * FROM ast_to_blocks('src/main.py');

-- Aggregate to LIST(duck_block) yourself (what ast_to_blocks_list does)
SELECT db_render_blocks(list(block ORDER BY element_order))
FROM ast_to_blocks('src/main.py')
GROUP BY file_path;

-- Convert a pre-parsed table (parse with peek := 'full' for complete bodies)
CREATE TABLE code AS SELECT * FROM read_ast('src/**/*.py', peek := 'full');
SELECT * FROM ast_to_blocks_from('code', style := 'summary');

Parameters (same tuning names on all three macros; language only on the parsing variants):

ParameterDefaultMeaning
style'outline''outline' = nested headings + code bodies for leaf definitions; 'summary' = headings + one-line signature paragraphs, no bodies; 'flat' = every definition at one heading level, each with its body. Unknown values raise an error.
include_bodiestruefalse suppresses code blocks in any style
include_metadatatrueLeading per-file metadata block (yaml: file_path, language, definition counts)
base_heading_level1Heading level of a top-level definition
max_heading_level6Demotion cap: definitions nested deeper render at this level

Spec conformance (duck_blocks v0.4.0): headings carry the semantic level in attributes['heading_level'] (as a string) with a NULL level field; code blocks carry attributes['language']; metadata blocks use encoding = 'yaml'; element_order starts at 0 per file and follows document order.

Body fidelity note: code-block content comes from the peek column — a presentation substrate, which is exactly what this macro is (peek is never used for correctness features). ast_to_blocks parses with peek := 'full' internally; for ast_to_blocks_from, body fidelity follows the input table's extraction config — peek := 'smart' input yields truncated bodies (not detectable per row), and an all-NULL peek column (peek := 'none') raises an error with a re-parse hint. No extraction configuration retains per-node source text (source := controls location columns only); exact-source extraction is planned v2 work.

Limitations

  • Parse-only: This analyzes syntax, not semantics (no type checking, symbol resolution, etc.)
  • Tree-sitter dependent: Parsing quality depends on Tree-sitter grammar completeness
  • Single-threaded parsing: Files are parsed sequentially (though results stream efficiently)
  • File-by-file processing: Each file is parsed independently (no cross-file analysis)

Use Cases

  • Code quality analysis - Find complexity hotspots and code smells
  • Dependency analysis - Understand import/include relationships
  • Refactoring assistance - Identify patterns and duplicated code
  • Documentation generation - Extract API signatures and comments
  • Security auditing - Find dangerous patterns across languages
  • Learning and exploration - Understand unfamiliar codebases quickly
  • Cross-language analysis - Compare patterns across different programming languages

Contributing

Bug reports, feature requests, and questions are welcome — please open an issue on this repository. (This is a DuckDB community extension: file issues here, not on the main duckdb/duckdb repo.) Pull requests are welcome too.

Adding a language

This project uses Tree-sitter grammars as git submodules. To add a new language:

  1. Add the grammar: git submodule add <grammar-repo> grammars/tree-sitter-<lang>
  2. Update CMakeLists.txt to build the grammar
  3. Add language adapter in src/language_adapters/
  4. Add type definitions in src/language_configs/

See docs/development/adding-languages.md for details.

License

Apache License 2.0 — see the LICENSE file. This extension bundles the Tree-sitter runtime and its language grammars (all MIT / Apache-2.0); see THIRD_PARTY_LICENSES.md and NOTICE for the bundled components and their copyright notices.

Documentation

API Reference

Guides


Sitting Duck transforms your source code into a sitting duck for SQL-based analysis. 🦆

Previous name: DuckDB AST Extension