@memberjunction/sql-converter
Overview
Section titled “Overview”A production-grade SQL dialect conversion pipeline that converts SQL Server (T-SQL) to PostgreSQL. The pipeline uses a rule-based architecture where each SQL statement type is handled by a specialized conversion rule, producing deterministic, executable PostgreSQL output.
Current status: 0 errors across the v5.0 baseline (273 stored procedures) + 6 incremental migrations. 465 unit tests passing.
Key Features
Section titled “Key Features”- Rule-based conversion with 11 specialized rules
- Statement classification (27 distinct types)
- Context-aware conversion (CREATE TABLE column types inform INSERT boolean casting)
- Ordered output grouping (Tables -> FKs -> Views -> Functions -> Triggers -> Data -> Grants -> Comments)
- Global post-processing with 25+ transformations
- Database verification and cross-database audit
- LLM fallback for unhandled patterns
Architecture
Section titled “Architecture”flowchart TD
A[SQL Server File] --> B[Preprocess<br/>Flyway placeholders]
B --> C[Split on GO<br/>Batch separator]
C --> D[SubSplitter<br/>Compound batches]
D --> E[StatementClassifier<br/>27 statement types]
E --> F{Statement Type}
F -->|SKIP_*| G[Skip / Discard]
F -->|Convertible| H[Match Rules<br/>by AppliesTo]
H --> I[PreProcess Phase]
I --> J{BypassSqlglot?}
J -->|Yes| K[Rule handles<br/>full conversion]
J -->|No| L[sqlglot transpile]
L --> M[PostProcess Phase]
K --> N[Route to<br/>OutputGroup]
M --> N
N --> O[Assemble Groups<br/>in dependency order]
O --> P[PostProcessor<br/>Global cleanup]
P --> Q[PostgreSQL Output]
Quick Start
Section titled “Quick Start”Installation
Section titled “Installation”npm install @memberjunction/sql-converterCLI Usage
Section titled “CLI Usage”npx mj sql-convert --source migrations/v5/Baseline.sql --output migrations/pg/Baseline_PG.sqlProgrammatic Usage
Section titled “Programmatic Usage”import { convertFile, getTSQLToPostgresRules } from '@memberjunction/sql-converter';
const result = convertFile({ Source: 'path/to/input.sql', SourceIsFile: true, OutputFile: 'path/to/output.sql', Rules: getTSQLToPostgresRules(), OnProgress: (msg) => console.log(msg),});
console.log(`Converted ${result.Stats.Converted} of ${result.Stats.TotalBatches} batches`);Pipeline Deep Dive
Section titled “Pipeline Deep Dive”sequenceDiagram
participant F as File Input
participant P as Preprocessor
participant S as Splitter
participant SS as SubSplitter
participant C as Classifier
participant R as Rules Engine
participant G as Output Groups
participant PP as PostProcessor
participant O as Output
F->>P: Raw SQL (replace ${flyway:defaultSchema} with __mj)
P->>S: Processed SQL
S->>SS: GO-separated batches
SS->>C: Sub-split individual statements
loop For each statement
C->>R: StatementType + SQL
alt SKIP type
R-->>G: Discard
else Has matching rule
R->>R: PreProcess (if defined)
alt BypassSqlglot = true
R->>R: PostProcess(original, original, context)
else BypassSqlglot = false
R->>R: sqlglot transpile
R->>R: PostProcess(transpiled, original, context)
end
R->>G: Route to appropriate group
end
end
G->>PP: Assembled output (Tables, FKs, Views, ...)
PP->>O: Final PostgreSQL SQL
Rule System
Section titled “Rule System”How Rules Work
Section titled “How Rules Work”Each rule implements IConversionRule:
interface IConversionRule { Name: string; // Display name AppliesTo: StatementType[]; // Which statement types Priority: number; // Lower = runs first BypassSqlglot?: boolean; // Skip sqlglot transpilation PreProcess?(sql: string, context: ConversionContext): string; PostProcess?(sql: string, originalSQL: string, context: ConversionContext): string;}Rule Priority Chain
Section titled “Rule Priority Chain”flowchart LR
A["CreateTableRule<br/>Priority 10"] --> B["ViewRule<br/>Priority 20"]
B --> C["ProcedureToFunctionRule<br/>Priority 30"]
C --> D["FunctionRule<br/>Priority 35"]
D --> E["TriggerRule<br/>Priority 40"]
E --> F["InsertRule<br/>Priority 50"]
F --> G["ConditionalDDLRule<br/>Priority 55"]
G --> H["AlterTableRule<br/>Priority 60"]
H --> I["CreateIndexRule<br/>Priority 70"]
I --> J["GrantRule<br/>Priority 80"]
J --> K["ExtendedPropertyRule<br/>Priority 90"]
ConversionContext
Section titled “ConversionContext”State accumulated across the pipeline:
interface ConversionContext { SourceDialect: string; // 'tsql' TargetDialect: string; // 'postgres' Schema: string; // '__mj' TableColumns: Map<string, Map<string, string>>; // table -> column -> PG type HandWrittenFunctions: Map<string, string>; // function name -> PG replacement CreatedFunctions: Set<string>; // successfully converted functions CreatedViews: Set<string>; // successfully converted views}flowchart LR
CT["CreateTableRule<br/>Populates TableColumns"] --> IR["InsertRule<br/>Reads TableColumns<br/>for boolean casting"]
P["ProcedureToFunctionRule<br/>Populates CreatedFunctions"] --> GR["GrantRule<br/>Reads CreatedFunctions<br/>to skip invalid grants"]
V["ViewRule<br/>Populates CreatedViews"] --> GR
Rule-by-Rule Documentation
Section titled “Rule-by-Rule Documentation”1. CreateTableRule (Priority 10)
Section titled “1. CreateTableRule (Priority 10)”Applies to: CREATE_TABLE | Bypasses sqlglot: Yes
Converts SQL Server CREATE TABLE statements to PostgreSQL DDL.
Conversions:
- Bracket identifiers
[schema].[table]toschema."table" - SQL Server types to PG types (see Type Mapping table below)
IDENTITY(1,1)toGENERATED BY DEFAULT AS IDENTITYDEFAULT (NEWSEQUENTIALID())toDEFAULT gen_random_uuid()DEFAULT (GETUTCDATE())toDEFAULT NOW()DEFAULT ((0))for BIT toDEFAULT FALSEDEFAULT ((1))for BIT toDEFAULT TRUE- Removes CLUSTERED/NONCLUSTERED, ON [PRIMARY], COLLATE
- Quotes PascalCase column names
- Tracks column types in
ConversionContext.TableColumns
Example:
-- Input (T-SQL)CREATE TABLE [__mj].[Users] ( [ID] [uniqueidentifier] NOT NULL DEFAULT (newsequentialid()), [Name] [nvarchar](100) NOT NULL, [IsActive] [bit] NOT NULL DEFAULT ((1)), CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([ID]))
-- Output (PostgreSQL)CREATE TABLE __mj."Users" ( "ID" UUID NOT NULL DEFAULT gen_random_uuid(), "Name" VARCHAR(100) NOT NULL, "IsActive" BOOLEAN NOT NULL DEFAULT TRUE, CONSTRAINT "PK_Users" PRIMARY KEY ("ID"));2. ViewRule (Priority 20)
Section titled “2. ViewRule (Priority 20)”Applies to: CREATE_VIEW | Bypasses sqlglot: Yes
Converts CREATE VIEW statements with full expression conversion.
Conversions:
CREATE [OR ALTER] VIEWtoCREATE OR REPLACE VIEWCROSS APPLYtoCROSS JOIN LATERALOUTER APPLYtoLEFT JOIN LATERAL- Adds
ON TRUEfor LEFT JOIN LATERAL without ON clause - Wraps LATERAL function calls:
func(args) AS aliasto(SELECT * FROM func(args)) AS alias - STRING_AGG WITHIN GROUP rewriting
- All expression helpers (date functions, CHARINDEX, STUFF, etc.)
- Column reference quoting:
alias.PascalColtoalias."PascalCol" - Skips views referencing sys.* tables or flyway
3. ProcedureToFunctionRule (Priority 30)
Section titled “3. ProcedureToFunctionRule (Priority 30)”Applies to: CREATE_PROCEDURE | Bypasses sqlglot: Yes
Converts SQL Server stored procedures to PostgreSQL functions.
Conversions:
- Parses parameters (name, type, default, OUTPUT flag)
- Maps SQL Server types to PG types for parameters
- Replaces
@varwithp_varin function body @@ROWCOUNTto_v_row_count,@@ERRORto0WHILE BEGIN...ENDtoWHILE...LOOP...END LOOPIF BEGIN...ENDtoIF...THEN...END IFSET @var =top_var :=- Extracts DECLARE statements into PG DECLARE block
INSERTEDtoNEW,DELETEDtoOLD- Determines return type (VOID, TABLE row type, SETOF)
- Wraps in
CREATE OR REPLACE FUNCTION ... $$ LANGUAGE plpgsql; - Skips procedures with sys.* references, temp tables, STRING_SPLIT
4. FunctionRule (Priority 35)
Section titled “4. FunctionRule (Priority 35)”Applies to: CREATE_FUNCTION | Bypasses sqlglot: Yes
Converts SQL Server functions (scalar and table-valued) to PostgreSQL.
Features:
- Checks for hand-written function replacements first (7 built-in: StripToAlphanumeric, GetProgrammaticName, ToTitleCase, ToProperCase, ExtractVersionComponents, parseEmail, parseDomain, fnInitials)
- Inline table-valued functions to
RETURNS TABLE(...) AS $$ ... $$ LANGUAGE sql; - Scalar functions to
RETURNS type AS $$ ... $$ LANGUAGE plpgsql; - WITH recursive CTE detection to
WITH RECURSIVE - DECLARE extraction, WHILE/IF conversion, type mapping
5. TriggerRule (Priority 40)
Section titled “5. TriggerRule (Priority 40)”Applies to: CREATE_TRIGGER | Bypasses sqlglot: Yes
Converts SQL Server triggers to PostgreSQL trigger functions.
Pattern:
-- InputCREATE TRIGGER trgAfterUpdate ON [__mj].[Users]AFTER UPDATE AS ...
-- OutputCREATE OR REPLACE FUNCTION __mj."trgAfterUpdate_func"() RETURNS TRIGGER AS $$BEGIN ... END;$$ LANGUAGE plpgsql;
CREATE TRIGGER "trgAfterUpdate" AFTER UPDATE ON __mj."Users"FOR EACH ROW EXECUTE FUNCTION __mj."trgAfterUpdate_func"();- Special handling for UpdatedAt triggers (common pattern)
- Skips triggers using TRIGGER_NESTLEVEL() or UPDATE()
INSERTEDtoNEW,DELETEDtoOLD
6. InsertRule (Priority 50)
Section titled “6. InsertRule (Priority 50)”Applies to: INSERT, UPDATE, DELETE | Bypasses sqlglot: Yes
Converts DML statements with expression helpers.
Order of operations (important for correctness):
- Convert identifiers
- Remove N prefix
- Remove COLLATE
- Convert common functions (BEFORE quoting to avoid quoting function names like LEN)
- String concatenation (
+to||) - CHARINDEX, STUFF, CONVERT, IIF, CAST types
- Quote PascalCase identifiers
- Ensure semicolon
7. ConditionalDDLRule (Priority 55)
Section titled “7. ConditionalDDLRule (Priority 55)”Applies to: CONDITIONAL_DDL | Bypasses sqlglot: Yes
Converts IF NOT EXISTS conditional DDL patterns.
Two patterns:
- Conditional Index:
IF NOT EXISTS (SELECT ... sys.indexes ...) CREATE INDEXtoCREATE INDEX IF NOT EXISTS - DO $$ Block:
IF NOT EXISTS (...) BEGIN ... ENDtoDO $$ BEGIN IF NOT EXISTS (...) THEN ... END IF; END $$;
Features:
- Type conversions (NVARCHAR to VARCHAR, BIT to BOOLEAN, etc.)
- INFORMATION_SCHEMA casing (PG requires lowercase)
- PascalCase identifier quoting (skips string literals to avoid corrupting UUIDs)
8. AlterTableRule (Priority 60)
Section titled “8. AlterTableRule (Priority 60)”Applies to: FK_CONSTRAINT, PK_CONSTRAINT, CHECK_CONSTRAINT, UNIQUE_CONSTRAINT, ENABLE_CONSTRAINT, ALTER_TABLE | Bypasses sqlglot: Yes
Converts ALTER TABLE statements and constraint definitions.
Conversions:
- Identifier conversion, COLLATE removal
- Type conversions for defaults and columns
GETUTCDATE()toNOW(),NEWSEQUENTIALID()togen_random_uuid()- Removes CLUSTERED/NONCLUSTERED, ASC/DESC in constraints, ON [PRIMARY]
- FK constraints: adds
DEFERRABLE INITIALLY DEFERRED - CHECK constraints: adds
NOT VALID WITH CHECK CHECK CONSTRAINTto comment (no-op in PG)
9. CreateIndexRule (Priority 70)
Section titled “9. CreateIndexRule (Priority 70)”Applies to: CREATE_INDEX | Bypasses sqlglot: Yes
Converts CREATE INDEX statements.
Conversions:
- Removes CLUSTERED/NONCLUSTERED
- Removes
WITH (PAD_INDEX = ...)option blocks - Removes ON [filegroup]
- Removes INCLUDE columns (simplified)
- Fixes boolean values:
=(1)to=TRUE,=(0)to=FALSE
10. GrantRule (Priority 80)
Section titled “10. GrantRule (Priority 80)”Applies to: GRANT, REVOKE | Bypasses sqlglot: Yes
Converts GRANT and REVOKE statements.
Features:
- Adds
FUNCTIONkeyword for EXECUTE grants (PG requirement) - Skips grants on functions not in
CreatedFunctionsset - Skips grants on views (vw*) not in
CreatedViewsset - Removes N prefix from strings
11. ExtendedPropertyRule (Priority 90)
Section titled “11. ExtendedPropertyRule (Priority 90)”Applies to: EXTENDED_PROPERTY | Bypasses sqlglot: Yes
Converts sp_addextendedproperty calls to PostgreSQL COMMENT statements.
Supports:
- Named parameters (
@name=,@value=, etc.) - Positional parameters
- TABLE to
COMMENT ON TABLE - VIEW to
COMMENT ON VIEW - COLUMN to
COMMENT ON COLUMN - PROCEDURE/FUNCTION to commented out (PG needs signature)
Statement Classification
Section titled “Statement Classification”The StatementClassifier uses pattern matching to assign one of 27 types:
| Type | Examples |
|---|---|
CREATE_TABLE | CREATE TABLE [schema].[name] (...) |
CREATE_VIEW | CREATE VIEW, CREATE OR ALTER VIEW |
CREATE_PROCEDURE | CREATE PROC[EDURE] (unless references sys.*) |
CREATE_FUNCTION | CREATE FUNCTION |
CREATE_TRIGGER | CREATE TRIGGER |
CREATE_INDEX | CREATE [UNIQUE] [NONCLUSTERED] INDEX |
ALTER_TABLE | ALTER TABLE ADD COLUMN, ALTER TABLE ALTER COLUMN |
FK_CONSTRAINT | ALTER TABLE ADD CONSTRAINT ... FOREIGN KEY |
PK_CONSTRAINT | ALTER TABLE ADD CONSTRAINT ... PRIMARY KEY |
CHECK_CONSTRAINT | ALTER TABLE ADD CONSTRAINT ... CHECK |
UNIQUE_CONSTRAINT | ALTER TABLE ADD CONSTRAINT ... UNIQUE |
ENABLE_CONSTRAINT | ALTER TABLE WITH CHECK CHECK CONSTRAINT |
INSERT / UPDATE / DELETE | DML statements |
GRANT / DENY / REVOKE | DCL statements |
CONDITIONAL_DDL | IF NOT EXISTS (...) BEGIN ... END |
EXTENDED_PROPERTY | sp_addextendedproperty |
COMMENT_ONLY | Pure comment blocks |
SKIP_SESSION | SET NUMERIC_ROUNDABORT, SET ANSI_PADDING, etc. |
SKIP_ERROR | IF @@ERROR |
SKIP_SQLSERVER | SQL Server-specific code |
SKIP_NOCHECK | WITH NOCHECK |
SKIP_PRINT | PRINT statements |
UNKNOWN | Unclassified statements |
Output Grouping
Section titled “Output Grouping”Converted statements are grouped in dependency order:
flowchart TD
A[DDL: Tables, PKs, Indexes] --> B[Helper Functions fn*]
B --> C[Views]
C --> D[Stored Procedures sp*]
D --> E[Triggers]
E --> F[Data: INSERT/UPDATE/DELETE]
F --> G[FK & CHECK Constraints]
G --> H[Grants]
H --> I[Comments]
I --> J[Other / Errors]
Type Mapping Reference
Section titled “Type Mapping Reference”| SQL Server | PostgreSQL | Notes |
|---|---|---|
UNIQUEIDENTIFIER | UUID | |
BIT | BOOLEAN | |
INT | INTEGER | |
BIGINT | BIGINT | Unchanged |
SMALLINT | SMALLINT | Unchanged |
TINYINT | SMALLINT | PG has no TINYINT |
FLOAT | DOUBLE PRECISION | |
REAL | REAL | Unchanged |
MONEY | NUMERIC(19,4) | |
SMALLMONEY | NUMERIC(10,4) | |
DECIMAL(p,s) | NUMERIC(p,s) | |
NVARCHAR(n) | VARCHAR(n) | |
NVARCHAR(MAX) | TEXT | |
VARCHAR(MAX) | TEXT | |
NCHAR(n) | CHAR(n) | |
NTEXT | TEXT | |
DATETIME | TIMESTAMPTZ | |
DATETIME2 | TIMESTAMPTZ | |
DATETIMEOFFSET | TIMESTAMPTZ | |
SMALLDATETIME | TIMESTAMPTZ | |
IMAGE | BYTEA | |
VARBINARY(n/MAX) | BYTEA | |
XML | XML | Unchanged |
SQL_VARIANT | TEXT | |
HIERARCHYID | TEXT | |
IDENTITY(1,1) | GENERATED BY DEFAULT AS IDENTITY | Column attribute |
Function Mapping Reference
Section titled “Function Mapping Reference”| T-SQL Function | PostgreSQL Equivalent |
|---|---|
ISNULL(a, b) | COALESCE(a, b) |
GETUTCDATE() | NOW() |
GETDATE() | NOW() |
SYSDATETIMEOFFSET() | NOW() |
NEWID() | gen_random_uuid() |
NEWSEQUENTIALID() | gen_random_uuid() |
LEN(s) | LENGTH(s) |
SCOPE_IDENTITY() | lastval() |
SUSER_SNAME() | current_user |
CHARINDEX(sub, str) | POSITION(sub IN str) |
STUFF(s, start, len, repl) | OVERLAY(s PLACING repl FROM start FOR len) |
DATEADD(unit, n, date) | (date + n * INTERVAL '1 unit') |
DATEDIFF(day, start, end) | EXTRACT(DAY FROM (end::TIMESTAMPTZ - start::TIMESTAMPTZ)) |
DATEPART(unit, date) | EXTRACT(field FROM date) |
CONVERT(type, expr) | CAST(expr AS mapped_type) |
IIF(cond, t, f) | CASE WHEN cond THEN t ELSE f END |
SELECT TOP N | SELECT ... LIMIT N |
'a' + 'b' (string concat) | 'a' || 'b' |
N'text' | 'text' (prefix removed) |
RAISERROR(msg, n, n) | RAISE EXCEPTION msg |
ExpressionHelpers
Section titled “ExpressionHelpers”Shared expression-level converters used across multiple rules:
convertIdentifiers—[schema].[name]toschema."name"convertDateFunctions— DATEADD, DATEDIFF, DATEPART with full unit mappingconvertCharIndex— 2-arg and 3-arg CHARINDEXconvertStuff— STUFF to OVERLAYconvertStringConcat—+to||for string contextsconvertIIF— Nested IIF with paren-aware argument splittingconvertTopToLimit— SELECT TOP N to LIMIT NconvertCastTypes— CAST AS type mappingsconvertConvertFunction— CONVERT(type, expr) to CASTremoveNPrefix— N’text’ to ‘text’removeCollate— Remove COLLATE clausesconvertCommonFunctions— ISNULL, GETUTCDATE, NEWID, LEN, etc.quotePascalCaseIdentifiers— Quote PascalCase identifiers while preserving strings and comments
PostProcessor
Section titled “PostProcessor”Global cleanup pass applied to the complete output. Key transformations:
- Bracket cleanup — Remove remaining
[brackets](preserve PG array access) - Session control removal — SET NOEXEC, SET NUMERIC_ROUNDABORT, SET XACT_ABORT
- RAISERROR to
RAISE EXCEPTION - Type name unquoting —
"UUID"to UUID,"BOOLEAN"to BOOLEAN, etc. - Remaining type conversions — nvarchar to VARCHAR, uniqueidentifier to UUID
- Boolean fixes —
=(1)to=TRUE,=(0)to=FALSE - session_replication_role —
'DEFAULT'to'origin' - PL/pgSQL fixes —
END\n$$toEND;\n$$, empty BEGIN blocks - FK clause ordering — DEFERRABLE after ON DELETE/UPDATE
- LIKE character classes — SQL Server patterns to PG regex
- CHECK constraint fixes —
len()toLENGTH(),isjson()to JSON validation - INFORMATION_SCHEMA — Force lowercase
- Schema-qualified identifiers — Quote unquoted PascalCase after
__mj. - ISNULL to COALESCE — Final pass
- IF OBJECT_ID DROP to
DROP ... IF EXISTS - GRANT EXEC to
GRANT EXECUTEwith DO $$ wrapper - CREATE INDEX to
CREATE INDEX IF NOT EXISTS - Long index names — Truncate to 63 chars with MD5 hash suffix
- Backslash line fix — Fix lines starting with
\in INSERT string literals - Excessive blank lines — Collapse 4+ blank lines to 3
Extensibility Architecture
Section titled “Extensibility Architecture”The converter is built on three extensible subsystems that enable support for new source/target dialect combinations beyond T-SQL → PostgreSQL.
RuleRegistry
Section titled “RuleRegistry”Central registry for SQL conversion rules. Supports multiple source→target dialect combinations with case-insensitive dialect lookup and automatic priority sorting.
import { RuleRegistry } from '@memberjunction/sql-converter';
// Register rules for a dialect pairRuleRegistry.RegisterAll(myRules);
// Retrieve sorted rules for a dialect pairconst rules = RuleRegistry.GetRules('tsql', 'postgres');
// Check available combinationsconst combos = RuleRegistry.GetRegisteredCombinations();// [{ Source: 'tsql', Target: 'postgres' }]
// Check if rules exist for a combinationRuleRegistry.HasRules('tsql', 'mysql'); // false| Method | Description |
|---|---|
Register(rule) | Register a single rule for its declared SourceDialect/TargetDialect |
RegisterAll(rules) | Register multiple rules at once |
GetRules(source, target) | Get sorted rules (by priority ascending) for a dialect pair |
GetRegisteredCombinations() | List all registered {Source, Target} pairs |
HasRules(source, target) | Check if rules exist for a combination |
Clear() | Clear all rules (for testing) |
Rules self-register via SourceDialect and TargetDialect properties on IConversionRule. The registry uses case-insensitive matching and returns rules sorted by Priority (ascending).
TypeResolver
Section titled “TypeResolver”Centralized SQL type resolution that delegates to @memberjunction/sql-dialect’s DataTypeMap with MJ-specific overrides. All type mapping in the converter goes through this module so knowledge lives in one place.
import { resolveType, parseTypeString } from '@memberjunction/sql-converter';
// Parse a SQL type string into componentsconst parsed = parseTypeString('NVARCHAR(255)');// { BaseName: 'NVARCHAR', Length: 255 }
// Resolve a T-SQL type to PostgreSQLresolveType('UNIQUEIDENTIFIER'); // 'UUID'resolveType('NVARCHAR(MAX)'); // 'TEXT'resolveType('DECIMAL(18,2)'); // 'NUMERIC(18,2)'resolveType('DATETIME'); // 'TIMESTAMPTZ' (MJ override)
// Resolve for inline CAST/CONVERT expressionsresolveInlineType('DATETIME2(7)'); // 'TIMESTAMPTZ'MJ-specific overrides (applied before the dialect TypeMap):
DATETIME,DATETIME2,SMALLDATETIME→TIMESTAMPTZ(MJ stores everything with timezone)NTEXT→TEXTSQL_VARIANT,HIERARCHYID→TEXT
To add support for a new target dialect, add a new branch in the internal getTypeMap() function that constructs the dialect’s TypeMap.
DialectHeaderBuilder
Section titled “DialectHeaderBuilder”Pluggable interface for generating dialect-specific output file headers. Each target dialect can register a header builder that produces the preamble needed for the converted SQL file.
import { registerHeaderBuilder, getHeaderBuilder, DialectHeaderBuilder } from '@memberjunction/sql-converter';
// Built-in: PostgreSQL header builder (auto-registered)const pgBuilder = getHeaderBuilder('postgres');const header = pgBuilder?.BuildHeader('__mj');// Generates: CREATE EXTENSION, CREATE SCHEMA, SET search_path, implicit int->bool cast
// Register a custom builder for a new dialectclass MySQLHeaderBuilder implements DialectHeaderBuilder { readonly TargetDialect = 'mysql'; BuildHeader(schema: string): string { return `SET NAMES utf8mb4;\nCREATE SCHEMA IF NOT EXISTS \`${schema}\`;\nUSE \`${schema}\`;\n`; }}registerHeaderBuilder(new MySQLHeaderBuilder());The built-in PostgreSQLHeaderBuilder generates:
CREATE EXTENSIONfor pgcrypto and uuid-ossp- Schema creation and
SET search_path SET standard_conforming_strings = on- Implicit
INTEGER → BOOLEANcast upgrade (for SQL Server BIT compatibility)
Extending the Pipeline
Section titled “Extending the Pipeline”Adding a New Rule (Existing Dialect Pair)
Section titled “Adding a New Rule (Existing Dialect Pair)”- Create a new file in
src/rules/:
import type { IConversionRule, ConversionContext, StatementType } from './types.js';
export class MyNewRule implements IConversionRule { Name = 'MyNewRule'; SourceDialect = 'tsql'; TargetDialect = 'postgres'; AppliesTo: StatementType[] = ['SOME_TYPE']; Priority = 65; // Between existing rules BypassSqlglot = true;
PostProcess(sql: string, originalSQL: string, context: ConversionContext): string { // Your conversion logic here return convertedSQL; }}- Register it in
TSQLToPostgresRules.ts:
import { MyNewRule } from './MyNewRule.js';
export function getTSQLToPostgresRules(): IConversionRule[] { return [ // ... existing rules new MyNewRule(), // Insert at correct priority position ];}- Add tests in
src/__tests__/MyNewRule.test.ts
Adding Rules for a New Dialect Direction
Section titled “Adding Rules for a New Dialect Direction”To support a new conversion direction (e.g., T-SQL → MySQL):
- Create rules implementing
IConversionRulewithSourceDialect = 'tsql'andTargetDialect = 'mysql' - Register rules with
RuleRegistry.RegisterAll(myMySQLRules) - Add a TypeMap branch in
TypeResolver.getTypeMap()for the new target dialect - Register a DialectHeaderBuilder for the new target via
registerHeaderBuilder() - Create a convenience function like
getTSQLToMySQLRules()for easy consumption
Adding New Type Mappings
Section titled “Adding New Type Mappings”Update type mappings in:
TypeResolver— centralized type resolution (preferred for new dialects)CreateTableRule— for CREATE TABLE column typesExpressionHelpers.convertCastTypes()— for CAST expressionsExpressionHelpers.convertConvertFunction()— for CONVERT() functionPostProcessor.postProcess()— for final cleanup pass
Verification & Cross-Database Audit
Section titled “Verification & Cross-Database Audit”Database Verification
Section titled “Database Verification”import { ConversionPipeline } from '@memberjunction/sql-converter';
const result = await new ConversionPipeline().Run({ source: 'input.sql', sourceIsFile: true, sourceDialect: 'tsql', targetDialect: 'postgres', verify: true, targetConnection: 'postgresql://user:pass@localhost:5432/testdb',});Audit Command
Section titled “Audit Command”npx mj sql-audit --source-connection "mssql://..." --target-connection "postgresql://..." --schema __mjCompares source and target databases:
- Missing objects (tables, views, functions, triggers, indexes)
- Row count mismatches
Testing
Section titled “Testing”Running Tests
Section titled “Running Tests”# All testscd packages/SQLConverter && npm run test
# Watch modecd packages/SQLConverter && npm run test:watchTest Organization
Section titled “Test Organization”src/__tests__/ StatementClassifier.test.ts # 60+ tests SubSplitter.test.ts # 13 tests ExpressionHelpers.test.ts # 100+ tests BatchConverter.test.ts # 22 tests PostProcessor.test.ts # 50+ tests SQLFileSplitter.test.ts # 24 tests DatabaseAuditor.test.ts # 12 tests CreateTableRule.test.ts # 31 tests ViewRule.test.ts # 15 tests ProcedureToFunctionRule.test.ts # 48 tests FunctionRule.test.ts # 20+ tests TriggerRule.test.ts # 13 tests InsertRule.test.ts # 20 tests AlterTableRule.test.ts # 13 tests ConditionalDDLRule.test.ts # 20+ tests CreateIndexRule.test.ts # 15+ tests GrantRule.test.ts # 15+ tests ExtendedPropertyRule.test.ts # 8 tests TSQLToPostgresRules.test.ts # 10+ tests ConversionPipeline.test.ts # 14 tests integration.test.ts # Full pipeline against real PGAdding Test Cases
Section titled “Adding Test Cases”Follow the established pattern:
import { describe, it, expect } from 'vitest';import { MyRule } from '../rules/MyRule.js';import { createConversionContext } from '../rules/types.js';
const rule = new MyRule();const context = createConversionContext('tsql', 'postgres');
function convert(sql: string): string { return rule.PostProcess!(sql, sql, context);}
describe('MyRule', () => { describe('metadata', () => { it('should have the correct name, priority, and applies-to types', () => { expect(rule.Name).toBe('MyRule'); expect(rule.Priority).toBe(65); }); });
describe('feature area', () => { it('should convert X to Y', () => { const result = convert('INPUT SQL'); expect(result).toContain('EXPECTED OUTPUT'); }); });});Dependencies
Section titled “Dependencies”@memberjunction/sqlglot-ts— Python sqlglot wrapper for transpilation fallbackpg(dev) — PostgreSQL client for integration testsvitest(dev) — Test framework
License
Section titled “License”ISC