rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
281 lines • 13.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DDLToFixtureConverter = void 0;
const MultiQuerySplitter_1 = require("../utils/MultiQuerySplitter");
const SqlParser_1 = require("../parsers/SqlParser");
const CreateTableQuery_1 = require("../models/CreateTableQuery");
const InsertQuery_1 = require("../models/InsertQuery");
const ValuesQuery_1 = require("../models/ValuesQuery");
const TableDefinitionModel_1 = require("../models/TableDefinitionModel");
const ValueComponent_1 = require("../models/ValueComponent");
const DDLStatements_1 = require("../models/DDLStatements");
const TableNameUtils_1 = require("../utils/TableNameUtils");
const SqlFormatter_1 = require("./SqlFormatter");
class DDLToFixtureConverter {
/**
* Converts DDL statements (CREATE TABLE) in the provided SQL text to a Fixture JSON object.
* Ignores non-DDL statements and parse errors.
*
* @param ddlSql The SQL text containing CREATE TABLE statements.
* @returns A Record representing the Fixture JSON.
*/
static convert(ddlSql) {
const splitResult = MultiQuerySplitter_1.MultiQuerySplitter.split(ddlSql);
const createTableQueries = [];
const insertQueries = [];
const alterTableStatements = [];
for (const query of splitResult.queries) {
if (query.isEmpty)
continue;
try {
const ast = SqlParser_1.SqlParser.parse(query.sql);
if (ast instanceof CreateTableQuery_1.CreateTableQuery) {
createTableQueries.push(ast);
}
else if (ast instanceof InsertQuery_1.InsertQuery) {
insertQueries.push(ast);
}
else if (ast instanceof DDLStatements_1.AlterTableStatement) {
alterTableStatements.push(ast);
}
}
catch (e) {
// Ignore parse errors for non-DDL or invalid SQL
}
}
const registry = (0, TableDefinitionModel_1.createTableDefinitionRegistryFromCreateTableQueries)(createTableQueries);
// Apply ALTER TABLE effects on top of CREATE TABLE so defaults reflect the final schema state.
// This is critical for ZTD: omitted INSERT columns must consult the correct default metadata.
this.applyAlterTableStatements(registry, alterTableStatements);
const fixtureJson = {};
// Initialize fixtureJson with empty rows
for (const [tableName, def] of Object.entries(registry)) {
fixtureJson[tableName] = {
columns: def.columns.map(col => ({
name: col.name,
type: col.typeName,
default: this.formatDefaultValue(col.defaultValue)
})),
rows: []
};
}
// Sequence counters for nextval defaults: table -> column -> counter
const sequences = {};
// Process INSERT statements
for (const insert of insertQueries) {
// Only support INSERT ... VALUES
if (!(insert.selectQuery instanceof ValuesQuery_1.ValuesQuery)) {
continue;
}
const tableName = insert.insertClause.source.getAliasName();
if (!tableName || !fixtureJson[tableName]) {
// Ignore INSERTs for unknown tables
continue;
}
const tableDef = registry[tableName];
const targetColumns = insert.insertClause.columns;
const valuesQuery = insert.selectQuery;
// If columns are not specified, use all columns in order
const columnNames = targetColumns
? targetColumns.map(c => c.name)
: tableDef.columns.map(c => c.name);
for (const tuple of valuesQuery.tuples) {
const row = {};
// Map provided values
for (let i = 0; i < columnNames.length; i++) {
const colName = columnNames[i];
if (i < tuple.values.length) {
const val = tuple.values[i];
row[colName] = this.extractValue(val);
}
}
// Fill missing columns with defaults
for (const colDef of tableDef.columns) {
if (row[colDef.name] !== undefined) {
continue;
}
// Check for NOT NULL constraint without default
// TableColumnDefinitionModel.required is true if NOT NULL and no default/identity
if (colDef.required) {
throw new Error(`Column '${colDef.name}' in table '${tableName}' cannot be null and has no default value.`);
}
const hasDefault = colDef.defaultValue !== null && colDef.defaultValue !== undefined;
if (hasDefault) {
const defaultValStr = this.formatDefaultValue(colDef.defaultValue);
if (defaultValStr) {
const trimmedDefault = defaultValStr.trim();
const lowerTrimmed = trimmedDefault.toLowerCase();
const stringLiteralValue = trimmedDefault.startsWith("'") && trimmedDefault.endsWith("'")
? trimmedDefault.slice(1, -1).replace(/''/g, "'")
: trimmedDefault;
// Normalize literal NULLs (even when quoted) into JS null
if (stringLiteralValue.toLowerCase() === 'null') {
row[colDef.name] = null;
}
else if (lowerTrimmed.includes('nextval')) {
// Handle nextval sequence
if (!sequences[tableName])
sequences[tableName] = {};
if (!sequences[tableName][colDef.name])
sequences[tableName][colDef.name] = 0;
sequences[tableName][colDef.name]++;
row[colDef.name] = sequences[tableName][colDef.name];
}
else if (lowerTrimmed.includes('now') || lowerTrimmed.includes('current_timestamp')) {
// Handle timestamp defaults - use a fixed date for consistency or current date
// Using a fixed date makes tests deterministic
row[colDef.name] = "2023-01-01 00:00:00";
}
else {
// Use the literal default value
// Try to unquote if it's a string literal
if (trimmedDefault.startsWith("'") && trimmedDefault.endsWith("'")) {
row[colDef.name] = stringLiteralValue;
}
else {
// Try to parse number/boolean/null
const num = Number(trimmedDefault);
if (!isNaN(num)) {
row[colDef.name] = num;
}
else {
const lower = trimmedDefault.toLowerCase();
if (lower === 'true') {
row[colDef.name] = true;
}
else if (lower === 'false') {
row[colDef.name] = false;
}
else if (lower === 'null') {
row[colDef.name] = null;
}
else {
row[colDef.name] = trimmedDefault;
}
}
}
}
}
}
else {
// Default to null
row[colDef.name] = null;
}
}
fixtureJson[tableName].rows.push(row);
}
}
return fixtureJson;
}
/**
* Applies `ALTER TABLE ... ALTER COLUMN ... SET/DROP DEFAULT` actions to the table registry.
*
* Uses normalization-based resolution to map `ALTER TABLE` targets to matching `CREATE TABLE` entries.
* Ambiguous or unresolved table names are skipped to avoid accidental cross-table updates.
*
* Only `AlterTableAlterColumnDefault` actions are processed; all other ALTER TABLE actions are ignored.
*
* @param registry The table definition registry built from `CREATE TABLE` statements.
* @param alterStatements The `ALTER TABLE` statements to apply.
*/
static applyAlterTableStatements(registry, alterStatements) {
var _a;
if (alterStatements.length === 0) {
return;
}
// Build a stable lookup so ALTER TABLE targets can find the matching CREATE TABLE definition.
const normalizedToKey = new Map();
const ambiguous = new Set();
for (const tableKey of Object.keys(registry)) {
const normalized = (0, TableNameUtils_1.normalizeTableName)(tableKey);
const existing = normalizedToKey.get(normalized);
if (existing && existing !== tableKey) {
normalizedToKey.delete(normalized);
ambiguous.add(normalized);
continue;
}
if (!ambiguous.has(normalized)) {
normalizedToKey.set(normalized, tableKey);
}
}
const resolveTableKey = (tableName) => {
// Prefer exact matching for schema-qualified names to avoid accidental cross-table updates.
if (registry[tableName]) {
return tableName;
}
const normalized = (0, TableNameUtils_1.normalizeTableName)(tableName);
if (ambiguous.has(normalized)) {
return undefined;
}
return normalizedToKey.get(normalized);
};
for (const alter of alterStatements) {
const tableName = alter.table.toString();
const registryKey = resolveTableKey(tableName);
if (!registryKey) {
continue;
}
const tableDef = registry[registryKey];
for (const action of alter.actions) {
if (!(action instanceof DDLStatements_1.AlterTableAlterColumnDefault)) {
continue;
}
const targetName = action.columnName.name;
const column = tableDef.columns.find((col) => col.name.toLowerCase() === targetName.toLowerCase());
if (!column) {
continue;
}
// Only flip `required` when we are certain: SET DEFAULT always makes the column optional;
// DROP DEFAULT only makes it required when it previously relied on an explicit default.
const hadDefault = column.defaultValue !== null && column.defaultValue !== undefined;
if (action.dropDefault) {
column.defaultValue = null;
if (column.isNotNull && hadDefault) {
column.required = true;
}
continue;
}
column.defaultValue = (_a = action.setDefault) !== null && _a !== void 0 ? _a : null;
column.required = false;
}
}
}
static extractValue(value) {
if (value instanceof ValueComponent_1.LiteralValue) {
return value.value;
}
// For other types, try to format to string
try {
const formatter = new SqlFormatter_1.SqlFormatter({ keywordCase: 'none' });
const { formattedSql } = formatter.format(value);
// Remove quotes if it looks like a string literal
if (formattedSql.startsWith("'") && formattedSql.endsWith("'")) {
return formattedSql.slice(1, -1).replace(/''/g, "'");
}
return formattedSql;
}
catch (e) {
return String(value);
}
}
static formatDefaultValue(value) {
if (value === null || value === undefined) {
return undefined;
}
if (typeof value === 'string') {
return value;
}
try {
// Use SqlFormatter to print the AST node
const formatter = new SqlFormatter_1.SqlFormatter({ keywordCase: 'none' });
const { formattedSql } = formatter.format(value);
return formattedSql;
}
catch (e) {
// Fallback if formatter fails
return String(value);
}
}
}
exports.DDLToFixtureConverter = DDLToFixtureConverter;
//# sourceMappingURL=DDLToFixtureConverter.js.map