Skip to content

sqldoc

SQL-first development

A pluggable compiler pipeline for SQL schemas. Tags in comments drive code generation, migrations, and custom plugins. Your .sql files are the source of truth.

See It in Action

Write standard SQL with tags in comments:

INPUT SQL
sql
-- @import '@sqldoc/ns-audit'
-- @import '@sqldoc/ns-codegen'
-- @import '@sqldoc/ns-docs'
-- @import '@sqldoc/ns-rls'
-- @import '@sqldoc/ns-validate'

-- @docs.description('User accounts for the application')
-- @audit(on: [insert])
-- @rls
-- @rls.policy(for: ALL, to: authenticated, using: 'user_id = current_setting(''app.user_id'')::int')
CREATE TABLE users (
  -- @codegen.rename('id')
  user_id SERIAL PRIMARY KEY,

  -- @validate.pattern('^.+@.+\..+$')
  email VARCHAR(100) NOT NULL,

  -- @audit.redact(strategy: omit)
  password_hash TEXT NOT NULL
);
COMPILED OUTPUT
sql
CREATE TABLE users (
  user_id SERIAL PRIMARY KEY,

  email VARCHAR(100) NOT NULL,

  password_hash TEXT NOT NULL
);

-- Generated by sqldoc
-- sqldoc: @audit(on: [insert])
CREATE TABLE IF NOT EXISTS "users_audit_log" (
  id BIGSERIAL PRIMARY KEY,
  table_name TEXT NOT NULL,
  operation TEXT NOT NULL,
  old_data JSONB,
  new_data JSONB,
  changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION "users_audit_fn"() RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO "users_audit_log" (table_name, operation, old_data, new_data, changed_at)
  VALUES (TG_TABLE_NAME, TG_OP, CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE row_to_json(OLD) END, row_to_json(NEW), now());
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "users_audit_trigger"
  AFTER INSERT ON "users"
  FOR EACH ROW EXECUTE FUNCTION "users_audit_fn"();
-- sqldoc: @rls
ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;
-- sqldoc: @rls.policy(for: ALL, to: authenticated, using: 'user_id = current_setting(''app.user_id'')::int')
CREATE POLICY "users_authenticated_all" ON "users"
  FOR ALL
  TO authenticated
  USING (user_id = current_setting();
-- sqldoc: @validate.pattern('^.+@.+\..+$')
ALTER TABLE "users" ADD CONSTRAINT "users_email_pattern" CHECK ("email" ~ '^.+@.+\..+$');

Tags compile to correct SQL for your dialect. The original SQL stays unchanged — tags are comments.

Namespace Plugins

13 plugins covering audit trails, security, validation, documentation, and more.

@anon
PostgreSQL Anonymizer security labels
@audit
Audit namespace plugin -- generates audit triggers for Postgres, MySQL, and SQLite. Postgres: PL/pgSQL trigger function + multi-event trigger with row_to_json. MySQL: Separate per-event triggers with inline bodies using JSON_OBJECT and explicit columns. SQLite: Separate per-event triggers with inline bodies using json_object and explicit columns.
@codegen
@comment
@deprecated
@docs
@history
History namespace plugin -- generates history tables and triggers for change tracking. Creates a {table}_history table mirroring the source table's columns plus history metadata (history_id, valid_from, valid_to, history_operation). BEFORE UPDATE/DELETE triggers copy the OLD row into the history table. Postgres: PL/pgSQL function + multi-event BEFORE trigger. MySQL: Separate per-event BEFORE triggers with explicit column enumeration. SQLite: Separate per-event BEFORE triggers with explicit column enumeration.
@lint
@pg
PostGraphile smart comments for PostgreSQL
@rls
Row-Level Security policies for PostgreSQL
@softdelete
Soft-delete namespace plugin -- generates deleted_at columns, active views, and cascade triggers. Postgres: ALTER TABLE + CREATE VIEW + PL/pgSQL cascade function + trigger. MySQL: ALTER TABLE + CREATE VIEW + per-event AFTER UPDATE cascade trigger. SQLite: ALTER TABLE + CREATE VIEW + per-event AFTER UPDATE cascade trigger.
@temporal
Temporal namespace plugin -- generates SCD Type 2 temporal tables. Adds valid_from/valid_to columns to the table, creates a current-rows view, and generates triggers for versioned row management: - INSERT: set valid_from=NOW(), valid_to=NULL - UPDATE: copy OLD row with valid_to=NOW(), set NEW.valid_from=NOW() - DELETE: archive the row (insert copy with valid_to=NOW()), then allow the delete to proceed Postgres: PL/pgSQL BEFORE trigger functions. MySQL: Limited support — DDL + view + INSERT trigger only (self-referential triggers not supported).
@validate

Code Generation Templates

26 templates across 15 languages. Generate typed code from your SQL schema.