Skip to content

feat: implement PostgreSQL DDL to SQLite DDL converter - #1

Merged
hiromaily merged 6 commits into
mainfrom
feature/implement-pg-to-sqlite-converter-tasks
Feb 20, 2026
Merged

hiromaily merged 6 commits into
mainfrom
feature/implement-pg-to-sqlite-converter-tasks

Conversation

@hiromaily

Copy link
Copy Markdown
Owner

Summary

  • Replace the yaml-lint codebase with a full pg2sqlc implementation that converts PostgreSQL 16 DDL schemas to SQLite3-compatible DDL
  • Implement a 7-stage pipeline: Parse → Normalize → Plan → Transform → Order → Render → Report
  • Add comprehensive test coverage: 70 unit tests + 6 golden tests, all passing with clean CI (make ci)

Architecture

core/src/
├── pg/           # PostgreSQL DDL parsing (sqlparser 0.53 wrapper)
│   ├── parser.rs     # PG DDL text → IR (CREATE TABLE/INDEX/SEQUENCE/TYPE/ALTER)
│   └── normalize.rs  # Schema filtering, identifier normalization
├── ir/           # Internal representation
│   ├── ident.rs      # Identifier/QualifiedName with SQLite quoting
│   ├── types.rs      # PgType (40+ variants) and SqliteType enums
│   ├── expr.rs       # Expression IR (literals, functions, casts, operators)
│   └── model.rs      # SchemaModel, Table, Column, Index, Constraint, etc.
├── transform/    # PG IR → SQLite IR transformation
│   ├── planner.rs      # ALTER merging, SERIAL/sequence resolution
│   ├── type_map.rs     # PgType → SqliteType affinity mapping
│   ├── expr_map.rs     # Expression conversion (now()→CURRENT_TIMESTAMP, etc.)
│   ├── constraint.rs   # PK/UNIQUE/FK/CHECK transformation
│   ├── index.rs        # Index conversion with method filtering
│   ├── name_resolve.rs # Schema stripping, collision handling
│   └── topo.rs         # Topological sort for FK dependencies
├── sqlite/       # SQLite DDL rendering
│   └── render.rs     # IR → DDL text with deterministic ordering
├── diagnostics/  # Warning/error system
│   ├── warning.rs    # 25+ warning codes with severity levels
│   └── reporter.rs   # Output formatting, strict mode enforcement
└── lib.rs        # Public API: convert_pg_ddl_to_sqlite()

Key Capabilities

  • Type mapping: 40+ PostgreSQL types → SQLite affinity types (INTEGER/TEXT/REAL/NUMERIC/BLOB)
  • Expression conversion: now() → CURRENT_TIMESTAMP, true/false → 1/0, cast stripping, nextval() removal
  • Constraint handling: ALTER TABLE merging, SERIAL → INTEGER PRIMARY KEY (rowid alias), FK with actions
  • Schema support: Schema filtering (public/custom/all), collision detection with schema__table prefixing
  • Ordering: Topological sort for foreign key dependency ordering with cycle detection
  • Diagnostics: 25+ warning codes across 4 severity levels (Info/Lossy/Unsupported/Error), strict mode
  • CLI: pg2sqlc with --input, --output, --schema, --enable-foreign-keys, --strict, --emit-warnings

Test plan

  • 70 unit tests covering all modules (ident, types, parser, normalize, type_map, expr_map, planner, constraint, index, topo, name_resolve, render, lib)
  • 6 golden tests comparing end-to-end conversion output against expected files
  • make ci passes (fmt-check + clippy -D warnings + all tests)
  • Manual testing with real-world PostgreSQL schema dumps

🤖 Generated with Claude Code

Replace the yaml-lint codebase with a full pg2sqlc implementation that
converts PostgreSQL 16 DDL schemas to SQLite3-compatible DDL.

Pipeline: Parse (sqlparser) → Normalize → Plan → Transform → Render

Key capabilities:
- Type mapping (40+ PG types → SQLite affinity types)
- Expression conversion (now()→CURRENT_TIMESTAMP, bool→0/1, cast stripping)
- ALTER TABLE constraint merging into CREATE TABLE
- SERIAL/BIGSERIAL → INTEGER PRIMARY KEY (rowid alias)
- Foreign key support with optional PRAGMA foreign_keys
- Topological sort for FK dependency ordering
- Schema filtering and collision handling
- Strict mode with comprehensive diagnostics (25+ warning codes)
- Golden test suite (6 fixtures) + 70 unit tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hiromaily hiromaily self-assigned this Feb 19, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @hiromaily, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a complete rewrite of the project, transforming it from a YAML linter into a dedicated tool for converting PostgreSQL 16 DDL schemas to SQLite3-compatible DDL. The core functionality is built around a robust, multi-stage pipeline designed for offline text-to-text conversion, ensuring accurate and comprehensive schema translation while providing detailed diagnostics for any lossy or unsupported features.

Highlights

  • Full pg2sqlc Implementation: The project has been completely rewritten to provide a robust tool for converting PostgreSQL 16 DDL schemas to SQLite3-compatible DDL.
  • 7-Stage Conversion Pipeline: A comprehensive pipeline has been implemented, covering parsing, normalization, planning, transformation, ordering, rendering, and reporting of DDL changes.
  • Comprehensive Test Coverage: Added 70 unit tests and 6 golden tests to ensure the accuracy and reliability of the conversion process, all passing with clean CI.
  • Key Capabilities: Includes advanced features such as mapping 40+ PostgreSQL types, converting expressions, handling various constraints, supporting schema filtering, topological sorting for foreign key dependencies, and a detailed diagnostics system.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .cursor/rules/project.mdc
    • Updated project overview, architecture, commands, and dependencies to reflect the DDL converter.
  • Cargo.lock
    • Added new dependency lock file for the project.
  • Cargo.toml
    • Updated workspace dependencies, removing YAML-related crates and updating thiserror.
  • cli/Cargo.toml
    • Renamed the CLI package to pg2sqlc.
    • Updated metadata including description, keywords, and categories.
    • Replaced YAML linter dependencies with DDL converter dependencies.
  • cli/src/main.rs
    • Rewrote the main CLI application logic to implement PostgreSQL to SQLite DDL conversion.
  • core/Cargo.toml
    • Renamed the core package to pg2sqlc-core.
    • Updated metadata including description, keywords, and categories.
    • Replaced YAML linter dependencies with sqlparser and thiserror.
  • core/src/config.rs
    • Removed the YAML linter configuration system.
  • core/src/diagnostics/mod.rs
    • Added the diagnostics module for conversion warnings.
  • core/src/diagnostics/reporter.rs
    • Added functionality for reporting conversion warnings and enforcing strict mode.
  • core/src/diagnostics/warning.rs
    • Defined warning types, severity levels, and constants for conversion diagnostics.
  • core/src/fixer.rs
    • Removed the YAML linter auto-fixer implementation.
  • core/src/ir/expr.rs
    • Added internal representation for SQL expressions used in DDL.
  • core/src/ir/ident.rs
    • Added internal representation for identifiers, including quoting logic.
  • core/src/ir/mod.rs
    • Updated the internal representation module to export new DDL-related types.
  • core/src/ir/model.rs
    • Added the core schema model for representing parsed DDL objects.
  • core/src/ir/types.rs
    • Added internal representation for PostgreSQL and SQLite data types.
  • core/src/lib.rs
    • Rewrote the core library to implement the PostgreSQL to SQLite DDL conversion pipeline.
  • core/src/linter.rs
    • Removed the YAML linter's main orchestration logic.
  • core/src/output/colored.rs
    • Removed the colored output formatter for the YAML linter.
  • core/src/output/mod.rs
    • Removed the output formatting module for the YAML linter.
  • core/src/output/parsable.rs
    • Removed the parsable output formatter for the YAML linter.
  • core/src/output/standard.rs
    • Removed the standard output formatter for the YAML linter.
  • core/src/pg/mod.rs
    • Added the PostgreSQL DDL parsing module.
  • core/src/pg/normalize.rs
    • Added logic for normalizing PostgreSQL DDL, including schema filtering.
  • core/src/pg/parser.rs
    • Implemented the PostgreSQL DDL parser using sqlparser-rs.
  • core/src/problem.rs
    • Removed the definition of lint problems.
  • core/src/rules/colons.rs
    • Removed the YAML lint rule for colon spacing.
  • core/src/rules/comments.rs
    • Removed the YAML lint rule for comment formatting.
  • core/src/rules/document_start.rs
    • Removed the YAML lint rule for document start markers.
  • core/src/rules/empty_lines.rs
    • Removed the YAML lint rule for empty lines.
  • core/src/rules/hyphens.rs
    • Removed the YAML lint rule for hyphen spacing.
  • core/src/rules/indentation.rs
    • Removed the YAML lint rule for indentation.
  • core/src/rules/key_duplicates.rs
    • Removed the YAML lint rule for duplicate keys.
  • core/src/rules/line_length.rs
    • Removed the YAML lint rule for line length.
  • core/src/rules/mod.rs
    • Removed the YAML linting rules module.
  • core/src/rules/new_line_at_end_of_file.rs
    • Removed the YAML lint rule for newlines at EOF.
  • core/src/rules/trailing_spaces.rs
    • Removed the YAML lint rule for trailing spaces.
  • core/src/rules/truthy.rs
    • Removed the YAML lint rule for truthy values.
  • core/src/sqlite/mod.rs
    • Added the SQLite DDL rendering module.
  • core/src/sqlite/render.rs
    • Implemented the SQLite DDL renderer from the internal representation.
  • core/src/transform/constraint.rs
    • Added logic for transforming PostgreSQL constraints to SQLite.
  • core/src/transform/expr_map.rs
    • Added logic for mapping PostgreSQL expressions to SQLite-compatible expressions.
  • core/src/transform/index.rs
    • Added logic for transforming PostgreSQL indexes to SQLite.
  • core/src/transform/mod.rs
    • Added the transformation module for DDL conversion.
  • core/src/transform/name_resolve.rs
    • Added logic for resolving and normalizing object names.
  • core/src/transform/planner.rs
    • Added planning logic to merge ALTERs and resolve SERIAL types.
  • core/src/transform/topo.rs
    • Added topological sorting for tables based on foreign key dependencies.
  • core/src/transform/type_map.rs
    • Added logic for mapping PostgreSQL data types to SQLite type affinities.
  • core/tests/config_options.rs
    • Removed old configuration option tests.
  • core/tests/golden_tests.rs
    • Added golden tests to validate end-to-end DDL conversion.
  • core/tests/integration_tests.rs
    • Removed old integration tests.
  • make/cli.mk
    • Updated Makefile targets for the new pg2sqlc CLI.
  • make/test.mk
    • Updated Makefile targets for testing, including new golden tests.
  • openspec/changes/pg-to-sqlite-converter/tasks.md
    • Marked all development tasks for the converter as completed.
  • tests/fixtures/basic_table.sql
    • Added a new SQL fixture for a basic table.
  • tests/fixtures/composite_pk.sql
    • Added a new SQL fixture for a table with a composite primary key.
  • tests/fixtures/foreign_keys.sql
    • Added a new SQL fixture demonstrating foreign key constraints.
  • tests/fixtures/serial_types.sql
    • Added a new SQL fixture for tables using SERIAL types.
  • tests/fixtures/various_types.sql
    • Added a new SQL fixture covering a variety of PostgreSQL data types.
  • tests/golden/basic_table.out.sql
    • Added the expected SQLite output for basic_table.sql.
  • tests/golden/composite_pk.out.sql
    • Added the expected SQLite output for composite_pk.sql.
  • tests/golden/foreign_keys.out.sql
    • Added the expected SQLite output for foreign_keys.sql without FKs enabled.
  • tests/golden/foreign_keys_enabled.out.sql
    • Added the expected SQLite output for foreign_keys.sql with FKs enabled.
  • tests/golden/serial_types.out.sql
    • Added the expected SQLite output for serial_types.sql.
  • tests/golden/various_types.out.sql
    • Added the expected SQLite output for various_types.sql.
Activity
  • The author has implemented a full conversion pipeline.
  • Comprehensive test coverage has been added, including 70 unit tests and 6 golden tests, all of which are passing.
  • CI checks (make ci) are clean.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request successfully implements a PostgreSQL 16 DDL to SQLite3 DDL converter, replacing the previous YAML linter codebase. The architecture follows a clear 7-stage pipeline and includes comprehensive unit and golden tests. The transformation logic handles type mapping, expression conversion, and topological sorting for foreign key dependencies. My feedback focuses on improving the robustness of name resolution in multi-schema scenarios, optimizing enum resolution, and ensuring that cycle detection in topological sorting is reported to the user.

Comment thread core/src/transform/planner.rs Outdated
Comment on lines +14 to +20
let alters = std::mem::take(&mut model.alter_constraints);

for alter in alters {
let target_table = model
.tables
.iter_mut()
.find(|t| t.name.name.normalized == alter.table.name.normalized);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Merging ALTER TABLE constraints currently only compares the table name, ignoring the schema. In multi-schema conversions (include_all_schemas: true), this can lead to constraints being merged into the wrong table if multiple schemas contain tables with the same name. The comparison should use the full QualifiedName.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread core/src/pg/parser.rs
Comment on lines +192 to +203
} => Some(TableConstraint::ForeignKey {
name: name.as_ref().map(|n| Ident::new(&n.value)),
columns: columns.iter().map(|c| Ident::new(&c.value)).collect(),
ref_table: convert_object_name(foreign_table),
ref_columns: referred_columns
.iter()
.map(|c| Ident::new(&c.value))
.collect(),
on_delete: on_delete.as_ref().and_then(convert_referential_action),
on_update: on_update.as_ref().and_then(convert_referential_action),
deferrable: false,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The parser hardcodes deferrable: false for foreign key constraints. PostgreSQL supports DEFERRABLE characteristics which should be extracted from the sqlparser AST to ensure accurate transformation and warning emission later in the pipeline.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just added comments for now

Comment thread core/src/pg/parser.rs Outdated
Comment on lines +325 to +350
let type_name = name.to_string().to_lowercase();
match type_name.as_str() {
"serial" => PgType::Serial,
"bigserial" => PgType::BigSerial,
"smallserial" => PgType::SmallSerial,
"inet" => PgType::Inet,
"cidr" => PgType::Cidr,
"macaddr" | "macaddr8" => PgType::MacAddr,
"money" => PgType::Money,
"xml" => PgType::Xml,
"point" => PgType::Point,
"line" => PgType::Line,
"lseg" => PgType::Lseg,
"box" => PgType::Box,
"path" => PgType::Path,
"polygon" => PgType::Polygon,
"circle" => PgType::Circle,
"int4range" => PgType::Int4Range,
"int8range" => PgType::Int8Range,
"numrange" => PgType::NumRange,
"tsrange" => PgType::TsRange,
"tstzrange" => PgType::TsTzRange,
"daterange" => PgType::DateRange,
_ => PgType::Other { name: type_name },
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The mapping for DataType::Custom uses the full string representation of the type name. If a custom type like serial is schema-qualified (e.g., pg_catalog.serial), the current match logic will fail to recognize it. It is safer to check the last part of the ObjectName.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines +136 to +152
/// Resolve enum columns: replace PgType::Other with PgType::Enum where a matching enum exists.
fn resolve_enums(model: &mut SchemaModel, _warnings: &mut [Warning]) {
let enum_names: Vec<(String, Vec<String>)> = model
.enums
.iter()
.map(|e| (e.name.name.normalized.clone(), e.values.clone()))
.collect();

for table in &mut model.tables {
for col in &mut table.columns {
if let PgType::Other { name } = &col.pg_type {
if let Some((_, _values)) = enum_names.iter().find(|(n, _)| n == name) {
col.pg_type = PgType::Enum { name: name.clone() };
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The enum resolution logic has O(T * C * E) complexity, where T is tables, C is columns, and E is enums. This can be optimized to O(T * C) by pre-collecting enum names into a HashSet for faster lookups.

fn resolve_enums(model: &mut SchemaModel, _warnings: &mut [Warning]) {
    let enum_names: std::collections::HashSet<String> = model
        .enums
        .iter()
        .map(|e| e.name.name.normalized.clone())
        .collect();

    for table in &mut model.tables {
        for col in &mut table.columns {
            if let PgType::Other { name } = &col.pg_type {
                if enum_names.contains(name) {
                    col.pg_type = PgType::Enum { name: name.clone() };
                }
            }
        }
    }
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread core/src/transform/topo.rs Outdated

/// Sort tables in dependency order (tables referenced by FKs come first).
/// Falls back to alphabetical order if cycles are detected.
pub fn topological_sort(tables: &mut Vec<Table>) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When a cycle is detected in the foreign key dependency graph, the topological sort falls back to alphabetical ordering without notifying the user. This can result in SQLite DDL that fails to execute if foreign keys are enabled. A warning should be emitted when a cycle is detected.

@hiromaily
hiromaily merged commit 0f72ca1 into main Feb 20, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant