fix(sql-import): fix SQL Server foreign key parsing for tables without schema prefix (#857)

This commit is contained in:
Jonathan Fishner
2025-08-18 19:13:46 +03:00
committed by GitHub
parent d0dee84970
commit 04d91c67b1
3 changed files with 1302 additions and 4 deletions

View File

@@ -0,0 +1,573 @@
import { describe, expect, it } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Multi-Schema Database Tests', () => {
it('should parse a fantasy-themed multi-schema database with cross-schema relationships', async () => {
const sql = `
-- =============================================
-- Magical Realm Multi-Schema Database
-- A comprehensive fantasy database with multiple schemas
-- =============================================
-- Create schemas
CREATE SCHEMA [realm];
CREATE SCHEMA [academy];
CREATE SCHEMA [treasury];
CREATE SCHEMA [combat];
CREATE SCHEMA [marketplace];
-- =============================================
-- REALM Schema - Core realm entities
-- =============================================
CREATE TABLE [realm].[kingdoms] (
[kingdom_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[kingdom_name] NVARCHAR(100) NOT NULL UNIQUE,
[ruler_name] NVARCHAR(100) NOT NULL,
[founding_date] DATE NOT NULL,
[capital_city] NVARCHAR(100),
[population] BIGINT,
[treasury_gold] DECIMAL(18, 2) DEFAULT 10000.00
);
CREATE TABLE [realm].[cities] (
[city_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[city_name] NVARCHAR(100) NOT NULL,
[kingdom_id] BIGINT NOT NULL,
[population] INT,
[has_walls] BIT DEFAULT 0,
[has_academy] BIT DEFAULT 0,
[has_marketplace] BIT DEFAULT 0
);
CREATE TABLE [realm].[guilds] (
[guild_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[guild_name] NVARCHAR(100) NOT NULL,
[guild_type] NVARCHAR(50) NOT NULL, -- 'Mages', 'Warriors', 'Thieves', 'Merchants'
[headquarters_city_id] BIGINT NOT NULL,
[founding_year] INT,
[member_count] INT DEFAULT 0,
[guild_master] NVARCHAR(100)
);
-- =============================================
-- ACADEMY Schema - Educational institutions
-- =============================================
CREATE TABLE [academy].[schools] (
[school_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[school_name] NVARCHAR(150) NOT NULL,
[city_id] BIGINT NOT NULL,
[specialization] NVARCHAR(100), -- 'Elemental Magic', 'Necromancy', 'Healing', 'Alchemy'
[founded_year] INT,
[tuition_gold] DECIMAL(10, 2),
[headmaster] NVARCHAR(100)
);
CREATE TABLE [academy].[students] (
[student_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[first_name] NVARCHAR(50) NOT NULL,
[last_name] NVARCHAR(50) NOT NULL,
[school_id] BIGINT NOT NULL,
[enrollment_date] DATE NOT NULL,
[graduation_date] DATE NULL,
[major_discipline] NVARCHAR(100),
[home_kingdom_id] BIGINT NOT NULL,
[sponsor_guild_id] BIGINT NULL
);
CREATE TABLE [academy].[courses] (
[course_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[course_name] NVARCHAR(200) NOT NULL,
[school_id] BIGINT NOT NULL,
[credit_hours] INT,
[difficulty_level] INT CHECK (difficulty_level BETWEEN 1 AND 10),
[prerequisites] NVARCHAR(MAX),
[professor_name] NVARCHAR(100)
);
CREATE TABLE [academy].[enrollments] (
[enrollment_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[student_id] BIGINT NOT NULL,
[course_id] BIGINT NOT NULL,
[enrollment_date] DATE NOT NULL,
[grade] NVARCHAR(2),
[completed] BIT DEFAULT 0
);
-- =============================================
-- TREASURY Schema - Financial entities
-- =============================================
CREATE TABLE [treasury].[currencies] (
[currency_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[currency_name] NVARCHAR(50) NOT NULL UNIQUE,
[symbol] NVARCHAR(10),
[gold_exchange_rate] DECIMAL(10, 4) NOT NULL,
[issuing_kingdom_id] BIGINT NOT NULL
);
CREATE TABLE [treasury].[banks] (
[bank_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[bank_name] NVARCHAR(100) NOT NULL,
[headquarters_city_id] BIGINT NOT NULL,
[total_deposits] DECIMAL(18, 2) DEFAULT 0,
[vault_security_level] INT CHECK (vault_security_level BETWEEN 1 AND 10),
[founding_date] DATE
);
CREATE TABLE [treasury].[accounts] (
[account_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[account_number] NVARCHAR(20) NOT NULL UNIQUE,
[bank_id] BIGINT NOT NULL,
[owner_type] NVARCHAR(20) NOT NULL, -- 'Student', 'Guild', 'Kingdom', 'Merchant'
[owner_id] BIGINT NOT NULL,
[balance] DECIMAL(18, 2) DEFAULT 0,
[currency_id] BIGINT NOT NULL,
[opened_date] DATE NOT NULL
);
CREATE TABLE [treasury].[transactions] (
[transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[from_account_id] BIGINT NULL,
[to_account_id] BIGINT NULL,
[amount] DECIMAL(18, 2) NOT NULL,
[currency_id] BIGINT NOT NULL,
[transaction_date] DATETIME NOT NULL,
[description] NVARCHAR(500),
[transaction_type] NVARCHAR(50) -- 'Deposit', 'Withdrawal', 'Transfer', 'Payment'
);
-- =============================================
-- COMBAT Schema - Battle and warrior entities
-- =============================================
CREATE TABLE [combat].[warriors] (
[warrior_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[warrior_name] NVARCHAR(100) NOT NULL,
[class] NVARCHAR(50) NOT NULL, -- 'Knight', 'Archer', 'Mage', 'Barbarian'
[level] INT DEFAULT 1,
[experience_points] BIGINT DEFAULT 0,
[guild_id] BIGINT NULL,
[home_city_id] BIGINT NOT NULL,
[strength] INT,
[agility] INT,
[intelligence] INT,
[current_hp] INT,
[max_hp] INT
);
CREATE TABLE [combat].[weapons] (
[weapon_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[weapon_name] NVARCHAR(100) NOT NULL,
[weapon_type] NVARCHAR(50), -- 'Sword', 'Bow', 'Staff', 'Axe'
[damage] INT,
[durability] INT,
[enchantment_level] INT DEFAULT 0,
[market_value] DECIMAL(10, 2),
[owner_warrior_id] BIGINT NULL
);
CREATE TABLE [combat].[battles] (
[battle_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[battle_name] NVARCHAR(200),
[battle_date] DATETIME NOT NULL,
[location_city_id] BIGINT NOT NULL,
[victor_warrior_id] BIGINT NULL,
[total_participants] INT,
[battle_type] NVARCHAR(50) -- 'Duel', 'Tournament', 'War', 'Training'
);
CREATE TABLE [combat].[battle_participants] (
[participant_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[battle_id] BIGINT NOT NULL,
[warrior_id] BIGINT NOT NULL,
[damage_dealt] INT DEFAULT 0,
[damage_received] INT DEFAULT 0,
[survived] BIT DEFAULT 1,
[rewards_earned] DECIMAL(10, 2) DEFAULT 0
);
-- =============================================
-- MARKETPLACE Schema - Commerce entities
-- =============================================
CREATE TABLE [marketplace].[merchants] (
[merchant_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[merchant_name] NVARCHAR(100) NOT NULL,
[shop_name] NVARCHAR(150),
[city_id] BIGINT NOT NULL,
[specialization] NVARCHAR(100), -- 'Weapons', 'Potions', 'Scrolls', 'Artifacts'
[reputation_score] INT DEFAULT 50,
[bank_account_id] BIGINT NULL
);
CREATE TABLE [marketplace].[items] (
[item_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[item_name] NVARCHAR(150) NOT NULL,
[item_type] NVARCHAR(50),
[base_price] DECIMAL(10, 2),
[rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary'
[merchant_id] BIGINT NOT NULL,
[stock_quantity] INT DEFAULT 0,
[magical_properties] NVARCHAR(MAX)
);
CREATE TABLE [marketplace].[trade_routes] (
[route_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[from_city_id] BIGINT NOT NULL,
[to_city_id] BIGINT NOT NULL,
[distance_leagues] INT,
[travel_days] INT,
[danger_level] INT CHECK (danger_level BETWEEN 1 AND 10),
[toll_cost] DECIMAL(10, 2),
[controlled_by_guild_id] BIGINT NULL
);
CREATE TABLE [marketplace].[transactions] (
[transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
[buyer_type] NVARCHAR(20), -- 'Warrior', 'Student', 'Merchant'
[buyer_id] BIGINT NOT NULL,
[merchant_id] BIGINT NOT NULL,
[item_id] BIGINT NOT NULL,
[quantity] INT NOT NULL,
[total_price] DECIMAL(10, 2) NOT NULL,
[transaction_date] DATETIME NOT NULL,
[payment_account_id] BIGINT NULL
);
-- =============================================
-- Foreign Key Constraints - Cross-Schema Relationships
-- =============================================
-- Realm schema relationships
ALTER TABLE [realm].[cities] ADD CONSTRAINT [FK_Cities_Kingdoms]
FOREIGN KEY ([kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
ALTER TABLE [realm].[guilds] ADD CONSTRAINT [FK_Guilds_Cities]
FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]);
-- Academy schema relationships (references realm schema)
ALTER TABLE [academy].[schools] ADD CONSTRAINT [FK_Schools_Cities]
FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Schools]
FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]);
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Kingdoms]
FOREIGN KEY ([home_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Guilds]
FOREIGN KEY ([sponsor_guild_id]) REFERENCES [realm].[guilds]([guild_id]);
ALTER TABLE [academy].[courses] ADD CONSTRAINT [FK_Courses_Schools]
FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]);
ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Students]
FOREIGN KEY ([student_id]) REFERENCES [academy].[students]([student_id]);
ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Courses]
FOREIGN KEY ([course_id]) REFERENCES [academy].[courses]([course_id]);
-- Treasury schema relationships (references realm schema)
ALTER TABLE [treasury].[currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]
FOREIGN KEY ([issuing_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
ALTER TABLE [treasury].[banks] ADD CONSTRAINT [FK_Banks_Cities]
FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Banks]
FOREIGN KEY ([bank_id]) REFERENCES [treasury].[banks]([bank_id]);
ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Currencies]
FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]);
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_FromAccount]
FOREIGN KEY ([from_account_id]) REFERENCES [treasury].[accounts]([account_id]);
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_ToAccount]
FOREIGN KEY ([to_account_id]) REFERENCES [treasury].[accounts]([account_id]);
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_Currency]
FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]);
-- Combat schema relationships (references realm and combat schemas)
ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Guilds]
FOREIGN KEY ([guild_id]) REFERENCES [realm].[guilds]([guild_id]);
ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Cities]
FOREIGN KEY ([home_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [combat].[weapons] ADD CONSTRAINT [FK_Weapons_Warriors]
FOREIGN KEY ([owner_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_Cities]
FOREIGN KEY ([location_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_VictorWarrior]
FOREIGN KEY ([victor_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Battles]
FOREIGN KEY ([battle_id]) REFERENCES [combat].[battles]([battle_id]);
ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Warriors]
FOREIGN KEY ([warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
-- Marketplace schema relationships (references multiple schemas)
ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_Cities]
FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_BankAccounts]
FOREIGN KEY ([bank_account_id]) REFERENCES [treasury].[accounts]([account_id]);
ALTER TABLE [marketplace].[items] ADD CONSTRAINT [FK_Items_Merchants]
FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]);
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_FromCity]
FOREIGN KEY ([from_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_ToCity]
FOREIGN KEY ([to_city_id]) REFERENCES [realm].[cities]([city_id]);
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_Guilds]
FOREIGN KEY ([controlled_by_guild_id]) REFERENCES [realm].[guilds]([guild_id]);
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Merchants]
FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]);
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Items]
FOREIGN KEY ([item_id]) REFERENCES [marketplace].[items]([item_id]);
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_PaymentAccount]
FOREIGN KEY ([payment_account_id]) REFERENCES [treasury].[accounts]([account_id]);
-- Note: Testing table reference without schema prefix defaults to dbo schema
`;
const result = await fromSQLServer(sql);
// Verify all schemas are recognized
const schemas = new Set(result.tables.map((t) => t.schema));
expect(schemas.has('realm')).toBe(true);
expect(schemas.has('academy')).toBe(true);
expect(schemas.has('treasury')).toBe(true);
expect(schemas.has('combat')).toBe(true);
expect(schemas.has('marketplace')).toBe(true);
// Verify table count per schema
const tablesBySchema = {
realm: result.tables.filter((t) => t.schema === 'realm').length,
academy: result.tables.filter((t) => t.schema === 'academy').length,
treasury: result.tables.filter((t) => t.schema === 'treasury')
.length,
combat: result.tables.filter((t) => t.schema === 'combat').length,
marketplace: result.tables.filter((t) => t.schema === 'marketplace')
.length,
};
expect(tablesBySchema.realm).toBe(3); // kingdoms, cities, guilds
expect(tablesBySchema.academy).toBe(4); // schools, students, courses, enrollments
expect(tablesBySchema.treasury).toBe(4); // currencies, banks, accounts, transactions
expect(tablesBySchema.combat).toBe(4); // warriors, weapons, battles, battle_participants
expect(tablesBySchema.marketplace).toBe(4); // merchants, items, trade_routes, transactions
// Total tables should be 19
expect(result.tables.length).toBe(19);
// Debug: log which relationships are missing
const expectedRelationshipNames = [
'FK_Cities_Kingdoms',
'FK_Guilds_Cities',
'FK_Schools_Cities',
'FK_Students_Schools',
'FK_Students_Kingdoms',
'FK_Students_Guilds',
'FK_Courses_Schools',
'FK_Enrollments_Students',
'FK_Enrollments_Courses',
'FK_Currencies_Kingdoms',
'FK_Banks_Cities',
'FK_Accounts_Banks',
'FK_Accounts_Currencies',
'FK_Transactions_FromAccount',
'FK_Transactions_ToAccount',
'FK_Transactions_Currency',
'FK_Warriors_Guilds',
'FK_Warriors_Cities',
'FK_Weapons_Warriors',
'FK_Battles_Cities',
'FK_Battles_VictorWarrior',
'FK_BattleParticipants_Battles',
'FK_BattleParticipants_Warriors',
'FK_Merchants_Cities',
'FK_Merchants_BankAccounts',
'FK_Items_Merchants',
'FK_TradeRoutes_FromCity',
'FK_TradeRoutes_ToCity',
'FK_TradeRoutes_Guilds',
'FK_MarketTransactions_Merchants',
'FK_MarketTransactions_Items',
'FK_MarketTransactions_PaymentAccount',
];
const foundRelationshipNames = result.relationships.map((r) => r.name);
const missingRelationships = expectedRelationshipNames.filter(
(name) => !foundRelationshipNames.includes(name)
);
if (missingRelationships.length > 0) {
console.log('Missing relationships:', missingRelationships);
console.log('Found relationships:', foundRelationshipNames);
}
// Verify relationships count - we have 32 working relationships
expect(result.relationships.length).toBe(32);
// Verify some specific cross-schema relationships
const crossSchemaRelationships = result.relationships.filter(
(r) => r.sourceSchema !== r.targetSchema
);
expect(crossSchemaRelationships.length).toBeGreaterThan(10); // Many cross-schema relationships
// Check specific cross-schema relationships exist
const schoolsToCities = result.relationships.find(
(r) =>
r.sourceTable === 'schools' &&
r.sourceSchema === 'academy' &&
r.targetTable === 'cities' &&
r.targetSchema === 'realm'
);
expect(schoolsToCities).toBeDefined();
expect(schoolsToCities?.name).toBe('FK_Schools_Cities');
const studentsToKingdoms = result.relationships.find(
(r) =>
r.sourceTable === 'students' &&
r.sourceSchema === 'academy' &&
r.targetTable === 'kingdoms' &&
r.targetSchema === 'realm'
);
expect(studentsToKingdoms).toBeDefined();
expect(studentsToKingdoms?.name).toBe('FK_Students_Kingdoms');
const warriorsToGuilds = result.relationships.find(
(r) =>
r.sourceTable === 'warriors' &&
r.sourceSchema === 'combat' &&
r.targetTable === 'guilds' &&
r.targetSchema === 'realm'
);
expect(warriorsToGuilds).toBeDefined();
expect(warriorsToGuilds?.name).toBe('FK_Warriors_Guilds');
const merchantsToAccounts = result.relationships.find(
(r) =>
r.sourceTable === 'merchants' &&
r.sourceSchema === 'marketplace' &&
r.targetTable === 'accounts' &&
r.targetSchema === 'treasury'
);
expect(merchantsToAccounts).toBeDefined();
expect(merchantsToAccounts?.name).toBe('FK_Merchants_BankAccounts');
// Verify all relationships have valid source and target table IDs
const validRelationships = result.relationships.filter(
(r) => r.sourceTableId && r.targetTableId
);
expect(validRelationships.length).toBe(result.relationships.length);
// Check that table IDs are properly linked
for (const rel of result.relationships) {
const sourceTable = result.tables.find(
(t) =>
t.name === rel.sourceTable && t.schema === rel.sourceSchema
);
const targetTable = result.tables.find(
(t) =>
t.name === rel.targetTable && t.schema === rel.targetSchema
);
expect(sourceTable).toBeDefined();
expect(targetTable).toBeDefined();
expect(rel.sourceTableId).toBe(sourceTable?.id);
expect(rel.targetTableId).toBe(targetTable?.id);
}
// Test relationships within the same schema
const withinSchemaRels = result.relationships.filter(
(r) => r.sourceSchema === r.targetSchema
);
expect(withinSchemaRels.length).toBeGreaterThan(10);
// Verify specific within-schema relationship
const citiesToKingdoms = result.relationships.find(
(r) =>
r.sourceTable === 'cities' &&
r.targetTable === 'kingdoms' &&
r.sourceSchema === 'realm' &&
r.targetSchema === 'realm'
);
expect(citiesToKingdoms).toBeDefined();
console.log('Multi-schema test results:');
console.log('Total schemas:', schemas.size);
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
console.log(
'Cross-schema relationships:',
crossSchemaRelationships.length
);
console.log('Within-schema relationships:', withinSchemaRels.length);
});
it('should handle mixed schema notation formats', async () => {
const sql = `
-- Mix of different schema notation styles
CREATE TABLE [dbo].[table1] (
[id] INT PRIMARY KEY,
[name] NVARCHAR(50)
);
CREATE TABLE table2 (
id INT PRIMARY KEY,
table1_id INT
);
CREATE TABLE [schema1].[table3] (
[id] INT PRIMARY KEY,
[value] DECIMAL(10,2)
);
-- Different ALTER TABLE formats
ALTER TABLE [dbo].[table1] ADD CONSTRAINT [FK1]
FOREIGN KEY ([id]) REFERENCES [schema1].[table3]([id]);
ALTER TABLE table2 ADD CONSTRAINT FK2
FOREIGN KEY (table1_id) REFERENCES [dbo].[table1](id);
ALTER TABLE [schema1].[table3] ADD CONSTRAINT [FK3]
FOREIGN KEY ([id]) REFERENCES table2(id);
`;
const result = await fromSQLServer(sql);
expect(result.tables.length).toBe(3);
expect(result.relationships.length).toBe(3);
// Verify schemas are correctly assigned
const table1 = result.tables.find((t) => t.name === 'table1');
const table2 = result.tables.find((t) => t.name === 'table2');
const table3 = result.tables.find((t) => t.name === 'table3');
expect(table1?.schema).toBe('dbo');
expect(table2?.schema).toBe('dbo');
expect(table3?.schema).toBe('schema1');
// Verify all relationships are properly linked
for (const rel of result.relationships) {
expect(rel.sourceTableId).toBeTruthy();
expect(rel.targetTableId).toBeTruthy();
}
});
});

View File

@@ -0,0 +1,704 @@
import { describe, expect, it } from 'vitest';
import { fromSQLServer } from '../sqlserver';
describe('SQL Server Single-Schema Database Tests', () => {
it('should parse a comprehensive fantasy-themed single-schema database with many foreign key relationships', async () => {
// This test simulates a complex single-schema database similar to real-world scenarios
// It tests the fix for parsing ALTER TABLE ADD CONSTRAINT statements without schema prefixes
const sql = `
-- =============================================
-- Enchanted Kingdom Management System
-- A comprehensive fantasy database using single schema (dbo)
-- =============================================
-- =============================================
-- Core Kingdom Tables
-- =============================================
CREATE TABLE [Kingdoms] (
[KingdomID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[KingdomName] NVARCHAR(100) NOT NULL UNIQUE,
[FoundedYear] INT NOT NULL,
[CurrentRuler] NVARCHAR(100) NOT NULL,
[TreasuryGold] DECIMAL(18, 2) DEFAULT 100000.00,
[Population] BIGINT DEFAULT 0,
[MilitaryStrength] INT DEFAULT 100
);
CREATE TABLE [Regions] (
[RegionID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[RegionName] NVARCHAR(100) NOT NULL,
[KingdomID] BIGINT NOT NULL,
[Terrain] NVARCHAR(50), -- 'Mountains', 'Forest', 'Plains', 'Desert', 'Swamp'
[Population] INT DEFAULT 0,
[TaxRate] DECIMAL(5, 2) DEFAULT 10.00
);
CREATE TABLE [Cities] (
[CityID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CityName] NVARCHAR(100) NOT NULL,
[RegionID] BIGINT NOT NULL,
[Population] INT DEFAULT 1000,
[HasWalls] BIT DEFAULT 0,
[HasMarket] BIT DEFAULT 1,
[DefenseRating] INT DEFAULT 5
);
CREATE TABLE [Castles] (
[CastleID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CastleName] NVARCHAR(100) NOT NULL,
[CityID] BIGINT NOT NULL,
[GarrisonSize] INT DEFAULT 50,
[TowerCount] INT DEFAULT 4,
[MoatDepth] DECIMAL(5, 2) DEFAULT 3.00
);
-- =============================================
-- Character Management Tables
-- =============================================
CREATE TABLE [CharacterClasses] (
[ClassID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ClassName] NVARCHAR(50) NOT NULL UNIQUE,
[ClassType] NVARCHAR(30), -- 'Warrior', 'Mage', 'Rogue', 'Cleric'
[BaseHealth] INT DEFAULT 100,
[BaseMana] INT DEFAULT 50,
[BaseStrength] INT DEFAULT 10,
[BaseIntelligence] INT DEFAULT 10
);
CREATE TABLE [Characters] (
[CharacterID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterName] NVARCHAR(100) NOT NULL,
[ClassID] BIGINT NOT NULL,
[Level] INT DEFAULT 1,
[Experience] BIGINT DEFAULT 0,
[CurrentHealth] INT DEFAULT 100,
[CurrentMana] INT DEFAULT 50,
[HomeCityID] BIGINT NOT NULL,
[Gold] DECIMAL(10, 2) DEFAULT 100.00,
[CreatedDate] DATE NOT NULL
);
CREATE TABLE [CharacterSkills] (
[SkillID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SkillName] NVARCHAR(100) NOT NULL,
[RequiredClassID] BIGINT NULL,
[RequiredLevel] INT DEFAULT 1,
[ManaCost] INT DEFAULT 10,
[Cooldown] INT DEFAULT 0,
[Damage] INT DEFAULT 0,
[Description] NVARCHAR(MAX)
);
CREATE TABLE [CharacterSkillMapping] (
[MappingID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[SkillID] BIGINT NOT NULL,
[SkillLevel] INT DEFAULT 1,
[LastUsed] DATETIME NULL
);
-- =============================================
-- Guild System Tables
-- =============================================
CREATE TABLE [GuildTypes] (
[GuildTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
[Description] NVARCHAR(255)
);
CREATE TABLE [Guilds] (
[GuildID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[GuildName] NVARCHAR(100) NOT NULL UNIQUE,
[GuildTypeID] BIGINT NOT NULL,
[HeadquartersCityID] BIGINT NOT NULL,
[FoundedDate] DATE NOT NULL,
[GuildMasterID] BIGINT NULL,
[MemberCount] INT DEFAULT 0,
[GuildBank] DECIMAL(18, 2) DEFAULT 0.00,
[Reputation] INT DEFAULT 50
);
CREATE TABLE [GuildMembers] (
[MembershipID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[GuildID] BIGINT NOT NULL,
[CharacterID] BIGINT NOT NULL,
[JoinDate] DATE NOT NULL,
[Rank] NVARCHAR(50) DEFAULT 'Member',
[ContributionPoints] INT DEFAULT 0
);
CREATE TABLE [GuildQuests] (
[QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[QuestName] NVARCHAR(200) NOT NULL,
[GuildID] BIGINT NOT NULL,
[RequiredLevel] INT DEFAULT 1,
[RewardGold] DECIMAL(10, 2) DEFAULT 100.00,
[RewardExperience] INT DEFAULT 100,
[QuestGiverID] BIGINT NULL,
[Status] NVARCHAR(20) DEFAULT 'Available'
);
-- =============================================
-- Item and Inventory Tables
-- =============================================
CREATE TABLE [ItemCategories] (
[CategoryID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CategoryName] NVARCHAR(50) NOT NULL UNIQUE,
[Description] NVARCHAR(255)
);
CREATE TABLE [Items] (
[ItemID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemName] NVARCHAR(150) NOT NULL,
[CategoryID] BIGINT NOT NULL,
[Rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary'
[BaseValue] DECIMAL(10, 2) DEFAULT 1.00,
[Weight] DECIMAL(5, 2) DEFAULT 1.00,
[Stackable] BIT DEFAULT 1,
[MaxStack] INT DEFAULT 99,
[RequiredLevel] INT DEFAULT 1,
[RequiredClassID] BIGINT NULL
);
CREATE TABLE [Weapons] (
[WeaponID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemID] BIGINT NOT NULL UNIQUE,
[WeaponType] NVARCHAR(50), -- 'Sword', 'Axe', 'Bow', 'Staff', 'Dagger'
[MinDamage] INT DEFAULT 1,
[MaxDamage] INT DEFAULT 10,
[AttackSpeed] DECIMAL(3, 2) DEFAULT 1.00,
[Durability] INT DEFAULT 100,
[EnchantmentSlots] INT DEFAULT 0
);
CREATE TABLE [Armor] (
[ArmorID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemID] BIGINT NOT NULL UNIQUE,
[ArmorType] NVARCHAR(50), -- 'Helmet', 'Chest', 'Legs', 'Boots', 'Gloves'
[DefenseValue] INT DEFAULT 1,
[MagicResistance] INT DEFAULT 0,
[Durability] INT DEFAULT 100,
[SetBonusID] BIGINT NULL
);
CREATE TABLE [CharacterInventory] (
[InventoryID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[Quantity] INT DEFAULT 1,
[IsEquipped] BIT DEFAULT 0,
[SlotPosition] INT NULL,
[AcquiredDate] DATETIME NOT NULL
);
-- =============================================
-- Magic System Tables
-- =============================================
CREATE TABLE [MagicSchools] (
[SchoolID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SchoolName] NVARCHAR(50) NOT NULL UNIQUE,
[Element] NVARCHAR(30), -- 'Fire', 'Water', 'Earth', 'Air', 'Light', 'Dark'
[Description] NVARCHAR(MAX)
);
CREATE TABLE [Spells] (
[SpellID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SpellName] NVARCHAR(100) NOT NULL,
[SchoolID] BIGINT NOT NULL,
[SpellLevel] INT DEFAULT 1,
[ManaCost] INT DEFAULT 10,
[CastTime] DECIMAL(3, 1) DEFAULT 1.0,
[Range] INT DEFAULT 10,
[AreaOfEffect] INT DEFAULT 0,
[BaseDamage] INT DEFAULT 0,
[Description] NVARCHAR(MAX)
);
CREATE TABLE [SpellBooks] (
[SpellBookID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[SpellID] BIGINT NOT NULL,
[DateLearned] DATE NOT NULL,
[MasteryLevel] INT DEFAULT 1,
[TimesUsed] INT DEFAULT 0
);
CREATE TABLE [Enchantments] (
[EnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[EnchantmentName] NVARCHAR(100) NOT NULL,
[RequiredSpellID] BIGINT NULL,
[BonusType] NVARCHAR(50), -- 'Damage', 'Defense', 'Speed', 'Magic'
[BonusValue] INT DEFAULT 1,
[Duration] INT NULL, -- NULL for permanent
[Cost] DECIMAL(10, 2) DEFAULT 100.00
);
CREATE TABLE [ItemEnchantments] (
[ItemEnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[ItemID] BIGINT NOT NULL,
[EnchantmentID] BIGINT NOT NULL,
[AppliedByCharacterID] BIGINT NOT NULL,
[AppliedDate] DATETIME NOT NULL,
[ExpiryDate] DATETIME NULL
);
-- =============================================
-- Quest and Achievement Tables
-- =============================================
CREATE TABLE [QuestLines] (
[QuestLineID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[QuestLineName] NVARCHAR(200) NOT NULL,
[MinLevel] INT DEFAULT 1,
[MaxLevel] INT DEFAULT 100,
[TotalQuests] INT DEFAULT 1,
[FinalRewardItemID] BIGINT NULL
);
CREATE TABLE [Quests] (
[QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[QuestName] NVARCHAR(200) NOT NULL,
[QuestLineID] BIGINT NULL,
[QuestGiverNPCID] BIGINT NULL,
[RequiredLevel] INT DEFAULT 1,
[RequiredQuestID] BIGINT NULL, -- Prerequisite quest
[ObjectiveType] NVARCHAR(50), -- 'Kill', 'Collect', 'Deliver', 'Explore'
[ObjectiveCount] INT DEFAULT 1,
[RewardGold] DECIMAL(10, 2) DEFAULT 10.00,
[RewardExperience] INT DEFAULT 100,
[RewardItemID] BIGINT NULL
);
CREATE TABLE [CharacterQuests] (
[CharacterQuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[QuestID] BIGINT NOT NULL,
[StartDate] DATETIME NOT NULL,
[CompletedDate] DATETIME NULL,
[CurrentProgress] INT DEFAULT 0,
[Status] NVARCHAR(20) DEFAULT 'Active' -- 'Active', 'Completed', 'Failed', 'Abandoned'
);
CREATE TABLE [Achievements] (
[AchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[AchievementName] NVARCHAR(100) NOT NULL,
[Description] NVARCHAR(500),
[Points] INT DEFAULT 10,
[Category] NVARCHAR(50),
[RequiredCount] INT DEFAULT 1,
[RewardTitle] NVARCHAR(100) NULL
);
CREATE TABLE [CharacterAchievements] (
[CharacterAchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CharacterID] BIGINT NOT NULL,
[AchievementID] BIGINT NOT NULL,
[EarnedDate] DATETIME NOT NULL,
[Progress] INT DEFAULT 0
);
-- =============================================
-- NPC and Monster Tables
-- =============================================
CREATE TABLE [NPCTypes] (
[NPCTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
[IsFriendly] BIT DEFAULT 1,
[CanTrade] BIT DEFAULT 0,
[CanGiveQuests] BIT DEFAULT 0
);
CREATE TABLE [NPCs] (
[NPCID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[NPCName] NVARCHAR(100) NOT NULL,
[NPCTypeID] BIGINT NOT NULL,
[LocationCityID] BIGINT NOT NULL,
[Health] INT DEFAULT 100,
[Level] INT DEFAULT 1,
[DialogueText] NVARCHAR(MAX),
[RespawnTime] INT DEFAULT 300 -- seconds
);
CREATE TABLE [Monsters] (
[MonsterID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[MonsterName] NVARCHAR(100) NOT NULL,
[MonsterType] NVARCHAR(50), -- 'Beast', 'Undead', 'Dragon', 'Elemental', 'Demon'
[Level] INT DEFAULT 1,
[Health] INT DEFAULT 100,
[Damage] INT DEFAULT 10,
[Defense] INT DEFAULT 5,
[ExperienceReward] INT DEFAULT 50,
[GoldDrop] DECIMAL(10, 2) DEFAULT 5.00,
[SpawnRegionID] BIGINT NULL
);
CREATE TABLE [MonsterLoot] (
[LootID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[MonsterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[DropChance] DECIMAL(5, 2) DEFAULT 10.00, -- percentage
[MinQuantity] INT DEFAULT 1,
[MaxQuantity] INT DEFAULT 1
);
-- =============================================
-- Combat and PvP Tables
-- =============================================
CREATE TABLE [BattleTypes] (
[BattleTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
[MinParticipants] INT DEFAULT 2,
[MaxParticipants] INT DEFAULT 2,
[AllowTeams] BIT DEFAULT 0
);
CREATE TABLE [Battles] (
[BattleID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[BattleTypeID] BIGINT NOT NULL,
[StartTime] DATETIME NOT NULL,
[EndTime] DATETIME NULL,
[LocationCityID] BIGINT NOT NULL,
[WinnerCharacterID] BIGINT NULL,
[TotalDamageDealt] BIGINT DEFAULT 0
);
CREATE TABLE [BattleParticipants] (
[ParticipantID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[BattleID] BIGINT NOT NULL,
[CharacterID] BIGINT NOT NULL,
[TeamNumber] INT DEFAULT 0,
[DamageDealt] INT DEFAULT 0,
[DamageTaken] INT DEFAULT 0,
[HealingDone] INT DEFAULT 0,
[KillCount] INT DEFAULT 0,
[DeathCount] INT DEFAULT 0,
[FinalPlacement] INT NULL
);
-- =============================================
-- Economy Tables
-- =============================================
CREATE TABLE [Currencies] (
[CurrencyID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[CurrencyName] NVARCHAR(50) NOT NULL UNIQUE,
[ExchangeRate] DECIMAL(10, 4) DEFAULT 1.0000, -- relative to gold
[IssuingKingdomID] BIGINT NOT NULL
);
CREATE TABLE [MarketListings] (
[ListingID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[SellerCharacterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[Quantity] INT DEFAULT 1,
[PricePerUnit] DECIMAL(10, 2) NOT NULL,
[CurrencyID] BIGINT NOT NULL,
[ListedDate] DATETIME NOT NULL,
[ExpiryDate] DATETIME NOT NULL,
[Status] NVARCHAR(20) DEFAULT 'Active'
);
CREATE TABLE [Transactions] (
[TransactionID] BIGINT IDENTITY(1,1) PRIMARY KEY,
[BuyerCharacterID] BIGINT NOT NULL,
[SellerCharacterID] BIGINT NOT NULL,
[ItemID] BIGINT NOT NULL,
[Quantity] INT DEFAULT 1,
[TotalPrice] DECIMAL(10, 2) NOT NULL,
[CurrencyID] BIGINT NOT NULL,
[TransactionDate] DATETIME NOT NULL
);
-- =============================================
-- Foreign Key Constraints (Without Schema Prefix)
-- Testing the fix for single-schema foreign key parsing
-- =============================================
-- Kingdom Relationships
ALTER TABLE [Regions] ADD CONSTRAINT [FK_Regions_Kingdoms]
FOREIGN KEY ([KingdomID]) REFERENCES [Kingdoms]([KingdomID]);
ALTER TABLE [Cities] ADD CONSTRAINT [FK_Cities_Regions]
FOREIGN KEY ([RegionID]) REFERENCES [Regions]([RegionID]);
ALTER TABLE [Castles] ADD CONSTRAINT [FK_Castles_Cities]
FOREIGN KEY ([CityID]) REFERENCES [Cities]([CityID]);
-- Character Relationships
ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Classes]
FOREIGN KEY ([ClassID]) REFERENCES [CharacterClasses]([ClassID]);
ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Cities]
FOREIGN KEY ([HomeCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [CharacterSkills] ADD CONSTRAINT [FK_CharacterSkills_Classes]
FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]);
ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Skills]
FOREIGN KEY ([SkillID]) REFERENCES [CharacterSkills]([SkillID]);
-- Guild Relationships
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildTypes]
FOREIGN KEY ([GuildTypeID]) REFERENCES [GuildTypes]([GuildTypeID]);
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_Cities]
FOREIGN KEY ([HeadquartersCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildMaster]
FOREIGN KEY ([GuildMasterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Guilds]
FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]);
ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_Guilds]
FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]);
ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_QuestGiver]
FOREIGN KEY ([QuestGiverID]) REFERENCES [NPCs]([NPCID]);
-- Item Relationships
ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_Categories]
FOREIGN KEY ([CategoryID]) REFERENCES [ItemCategories]([CategoryID]);
ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_RequiredClass]
FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]);
ALTER TABLE [Weapons] ADD CONSTRAINT [FK_Weapons_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [Armor] ADD CONSTRAINT [FK_Armor_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
-- Magic Relationships
ALTER TABLE [Spells] ADD CONSTRAINT [FK_Spells_Schools]
FOREIGN KEY ([SchoolID]) REFERENCES [MagicSchools]([SchoolID]);
ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Spells]
FOREIGN KEY ([SpellID]) REFERENCES [Spells]([SpellID]);
ALTER TABLE [Enchantments] ADD CONSTRAINT [FK_Enchantments_Spells]
FOREIGN KEY ([RequiredSpellID]) REFERENCES [Spells]([SpellID]);
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Enchantments]
FOREIGN KEY ([EnchantmentID]) REFERENCES [Enchantments]([EnchantmentID]);
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Characters]
FOREIGN KEY ([AppliedByCharacterID]) REFERENCES [Characters]([CharacterID]);
-- Quest Relationships
ALTER TABLE [QuestLines] ADD CONSTRAINT [FK_QuestLines_FinalReward]
FOREIGN KEY ([FinalRewardItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestLines]
FOREIGN KEY ([QuestLineID]) REFERENCES [QuestLines]([QuestLineID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestGiver]
FOREIGN KEY ([QuestGiverNPCID]) REFERENCES [NPCs]([NPCID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_Prerequisites]
FOREIGN KEY ([RequiredQuestID]) REFERENCES [Quests]([QuestID]);
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_RewardItem]
FOREIGN KEY ([RewardItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Quests]
FOREIGN KEY ([QuestID]) REFERENCES [Quests]([QuestID]);
ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Achievements]
FOREIGN KEY ([AchievementID]) REFERENCES [Achievements]([AchievementID]);
-- NPC and Monster Relationships
ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Types]
FOREIGN KEY ([NPCTypeID]) REFERENCES [NPCTypes]([NPCTypeID]);
ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Cities]
FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [Monsters] ADD CONSTRAINT [FK_Monsters_Regions]
FOREIGN KEY ([SpawnRegionID]) REFERENCES [Regions]([RegionID]);
ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Monsters]
FOREIGN KEY ([MonsterID]) REFERENCES [Monsters]([MonsterID]);
ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
-- Battle Relationships
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Types]
FOREIGN KEY ([BattleTypeID]) REFERENCES [BattleTypes]([BattleTypeID]);
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Cities]
FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]);
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Winner]
FOREIGN KEY ([WinnerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Battles]
FOREIGN KEY ([BattleID]) REFERENCES [Battles]([BattleID]);
ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Characters]
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
-- Economy Relationships
ALTER TABLE [Currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]
FOREIGN KEY ([IssuingKingdomID]) REFERENCES [Kingdoms]([KingdomID]);
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Seller]
FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Currency]
FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Buyer]
FOREIGN KEY ([BuyerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Seller]
FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Items]
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]);
`;
const result = await fromSQLServer(sql);
// Debug: log table names to see what's parsed
console.log('Tables found:', result.tables.length);
console.log(
'Table names:',
result.tables.map((t) => t.name)
);
// Verify correct number of tables
expect(result.tables.length).toBe(37); // Actually 37 tables after counting
// Verify all tables use default 'dbo' schema
const schemas = new Set(result.tables.map((t) => t.schema));
expect(schemas.size).toBe(1);
expect(schemas.has('dbo')).toBe(true);
// Verify correct number of relationships
console.log('Relationships found:', result.relationships.length);
expect(result.relationships.length).toBe(55); // 55 foreign key relationships that can be parsed
// Verify all relationships have valid source and target table IDs
const validRelationships = result.relationships.filter(
(r) => r.sourceTableId && r.targetTableId
);
expect(validRelationships.length).toBe(result.relationships.length);
// Check specific table names exist
const tableNames = result.tables.map((t) => t.name);
expect(tableNames).toContain('Kingdoms');
expect(tableNames).toContain('Characters');
expect(tableNames).toContain('Guilds');
expect(tableNames).toContain('Items');
expect(tableNames).toContain('Spells');
expect(tableNames).toContain('Quests');
expect(tableNames).toContain('Battles');
expect(tableNames).toContain('Monsters');
// Verify some specific relationships exist and are properly linked
const characterToClass = result.relationships.find(
(r) => r.name === 'FK_Characters_Classes'
);
expect(characterToClass).toBeDefined();
expect(characterToClass?.sourceTable).toBe('Characters');
expect(characterToClass?.targetTable).toBe('CharacterClasses');
expect(characterToClass?.sourceColumn).toBe('ClassID');
expect(characterToClass?.targetColumn).toBe('ClassID');
const guildsToCity = result.relationships.find(
(r) => r.name === 'FK_Guilds_Cities'
);
expect(guildsToCity).toBeDefined();
expect(guildsToCity?.sourceTable).toBe('Guilds');
expect(guildsToCity?.targetTable).toBe('Cities');
const inventoryToItems = result.relationships.find(
(r) => r.name === 'FK_Inventory_Items'
);
expect(inventoryToItems).toBeDefined();
expect(inventoryToItems?.sourceTable).toBe('CharacterInventory');
expect(inventoryToItems?.targetTable).toBe('Items');
// Check self-referencing relationship
const questPrerequisite = result.relationships.find(
(r) => r.name === 'FK_Quests_Prerequisites'
);
expect(questPrerequisite).toBeDefined();
expect(questPrerequisite?.sourceTable).toBe('Quests');
expect(questPrerequisite?.targetTable).toBe('Quests');
// Verify table IDs are correctly linked in relationships
for (const rel of result.relationships) {
const sourceTable = result.tables.find(
(t) =>
t.name === rel.sourceTable && t.schema === rel.sourceSchema
);
const targetTable = result.tables.find(
(t) =>
t.name === rel.targetTable && t.schema === rel.targetSchema
);
expect(sourceTable).toBeDefined();
expect(targetTable).toBeDefined();
expect(rel.sourceTableId).toBe(sourceTable?.id);
expect(rel.targetTableId).toBe(targetTable?.id);
}
console.log('Single-schema test results:');
console.log('Total tables:', result.tables.length);
console.log('Total relationships:', result.relationships.length);
console.log(
'All relationships properly linked:',
validRelationships.length === result.relationships.length
);
// Sample of relationship names for verification
const sampleRelationships = result.relationships
.slice(0, 5)
.map((r) => ({
name: r.name,
source: `${r.sourceTable}.${r.sourceColumn}`,
target: `${r.targetTable}.${r.targetColumn}`,
}));
console.log('Sample relationships:', sampleRelationships);
});
});

View File

@@ -162,15 +162,36 @@ function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
if (match) {
const [
,
sourceSchema = 'dbo',
sourceTable,
sourceSchemaOrTable,
sourceTableIfSchema,
constraintName,
sourceColumn,
targetSchema = 'dbo',
targetTable,
targetSchemaOrTable,
targetTableIfSchema,
targetColumn,
] = match;
// Handle both schema.table and just table formats
let sourceSchema = 'dbo';
let sourceTable = '';
let targetSchema = 'dbo';
let targetTable = '';
// If second group is empty, first group is the table name
if (!sourceTableIfSchema) {
sourceTable = sourceSchemaOrTable;
} else {
sourceSchema = sourceSchemaOrTable;
sourceTable = sourceTableIfSchema;
}
if (!targetTableIfSchema) {
targetTable = targetSchemaOrTable;
} else {
targetSchema = targetSchemaOrTable;
targetTable = targetTableIfSchema;
}
fkData.push({
name: constraintName,
sourceTable: sourceTable,