@linked-db/linked-ql
Version:
A query client that extends standard SQL with new syntax sugars and enables auto-versioning capabilities on any database
386 lines (335 loc) • 17.6 kB
JavaScript
import { $describe, $it, testParseAndStringify } from './00.parser.js';
$describe('Parser - DDL Constraints', () => {
// ---------------------
// COLUMN CONSTRAINTS
// ---------------------
$describe('Column Constraints', () => {
$it('should parse NULL and NOT NULL constraints', async () => {
await testParseAndStringify('ColumnNullConstraint', 'NOT NULL');
await testParseAndStringify('ColumnNullConstraint', 'NULL');
await testParseAndStringify('ColumnNullConstraint', 'CONSTRAINT c_null NULL');
});
$it('should parse DEFAULT constraints', async () => {
await testParseAndStringify('ColumnDefaultConstraint', 'DEFAULT 0');
await testParseAndStringify('ColumnDefaultConstraint', 'DEFAULT NULL');
await testParseAndStringify('ColumnDefaultConstraint', 'DEFAULT CURRENT_TIMESTAMP');
await testParseAndStringify('ColumnDefaultConstraint', 'CONSTRAINT def_key DEFAULT \'abc\'');
});
$it('should parse CHECK constraints', async () => {
await testParseAndStringify('CheckConstraint', 'CHECK (age > 0)');
await testParseAndStringify('CheckConstraint', 'CHECK (salary BETWEEN 1000 AND 5000) NO INHERIT');
await testParseAndStringify('CheckConstraint', 'CHECK (status IN (\'active\', \'inactive\'))');
await testParseAndStringify('CheckConstraint', 'CONSTRAINT chk_age CHECK (age > 0)');
});
$it('should parse GENERATED ALWAYS AS constraints (stored/generated)', async () => {
await testParseAndStringify('ColumnExpressionConstraint', 'GENERATED ALWAYS AS (col1 + col2) STORED');
await testParseAndStringify('ColumnExpressionConstraint', 'CONSTRAINT gen_expr GENERATED ALWAYS AS (LOWER(name)) STORED');
});
$it('should parse GENERATED ... AS IDENTITY constraints', async () => {
await testParseAndStringify('ColumnIdentityConstraint', 'GENERATED BY DEFAULT AS IDENTITY');
await testParseAndStringify('ColumnIdentityConstraint', 'GENERATED ALWAYS AS IDENTITY');
await testParseAndStringify('ColumnIdentityConstraint', 'CONSTRAINT id1 GENERATED ALWAYS AS IDENTITY');
return; // TODO
await testParseAndStringify('ColumnIdentityConstraint', 'GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1)');
});
$it('should parse PRIMARY KEY and UNIQUE constraints', async () => {
await testParseAndStringify('ColumnPKConstraint', 'PRIMARY KEY');
await testParseAndStringify('ColumnPKConstraint', 'CONSTRAINT pk PRIMARY KEY');
await testParseAndStringify('ColumnUKConstraint', 'UNIQUE');
await testParseAndStringify('ColumnUKConstraint', 'CONSTRAINT uk UNIQUE');
});
$it('should parse UNIQUE with NULLS DISTINCT / NOT DISTINCT', async () => {
await testParseAndStringify('ColumnUKConstraint', 'UNIQUE NULLS DISTINCT');
await testParseAndStringify('ColumnUKConstraint', 'UNIQUE NULLS NOT DISTINCT');
});
$it('should parse column-level REFERENCES (foreign key) constraints', async () => {
await testParseAndStringify('ColumnFKConstraint', 'REFERENCES users (id)');
await testParseAndStringify('ColumnFKConstraint', 'CONSTRAINT fk1 REFERENCES users (id)');
await testParseAndStringify('ColumnFKConstraint', 'REFERENCES users (id) MATCH FULL');
await testParseAndStringify('ColumnFKConstraint', 'REFERENCES users (id) ON DELETE CASCADE');
await testParseAndStringify('ColumnFKConstraint', 'REFERENCES users (id) ON UPDATE SET NULL');
await testParseAndStringify('ColumnFKConstraint', 'REFERENCES users (id) MATCH FULL ON DELETE SET NULL ON UPDATE SET DEFAULT');
await testParseAndStringify('ColumnFKConstraint', 'REFERENCES users');
await testParseAndStringify('ColumnFKConstraint', 'REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED', { dialect: 'postgres' });
});
});
// ---------------------
// TABLE CONSTRAINTS
// ---------------------
$describe('Table Constraints', () => {
$it('should parse PRIMARY KEY constraints', async () => {
await testParseAndStringify('TablePKConstraint', 'CONSTRAINT pk_id PRIMARY KEY (id)');
await testParseAndStringify('TablePKConstraint', 'PRIMARY KEY (id)');
await testParseAndStringify('TablePKConstraint', 'PRIMARY KEY (id, name)');
});
$it('should parse UNIQUE constraints', async () => {
await testParseAndStringify('TableUKConstraint', 'CONSTRAINT uk_email UNIQUE (email)');
await testParseAndStringify('TableUKConstraint', 'UNIQUE (email)');
await testParseAndStringify('TableUKConstraint', 'UNIQUE (email, username)');
await testParseAndStringify('TableUKConstraint', 'UNIQUE NULLS DISTINCT (email)');
await testParseAndStringify('TableUKConstraint', 'UNIQUE NULLS NOT DISTINCT (email)');
});
$it('should parse table-level CHECK constraints', async () => {
await testParseAndStringify('CheckConstraint', 'CHECK (amount > 0)');
await testParseAndStringify('CheckConstraint', 'CONSTRAINT chk CHECK (amount > 0) NO INHERIT');
});
$it('should parse FOREIGN KEY constraints with match + actions', async () => {
await testParseAndStringify('TableFKConstraint', 'CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)');
await testParseAndStringify('TableFKConstraint', 'FOREIGN KEY (user_id) REFERENCES users (id)');
await testParseAndStringify('TableFKConstraint', 'CONSTRAINT fk_user FOREIGN KEY (user_id, group_id) REFERENCES users (id, group_id) ON DELETE CASCADE ON UPDATE SET NULL');
await testParseAndStringify('TableFKConstraint', 'FOREIGN KEY (user_id) REFERENCES users');
await testParseAndStringify('TableFKConstraint', 'FOREIGN KEY (user_id) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED', { dialect: 'postgres' });
await testParseAndStringify('TableFKConstraint', 'FOREIGN KEY (col1, col2) REFERENCES ref_table (ref1, ref2) ON DELETE SET DEFAULT (col1) ON UPDATE SET NULL (col2)');
});
$it('should parse EXCLUDE constraints', async () => {
await testParseAndStringify('PGTableEXConstraint', 'EXCLUDE USING GIST (c WITH &&)');
await testParseAndStringify('PGTableEXConstraint', 'EXCLUDE USING GIST ((lower(col)) WITH =, other_col WITH &&)');
const sql =
`EXCLUDE USING GIST ((
lower(col1)
) WITH =, col2 WITH &&) INCLUDE (extra) WITH (fillfactor = 80) USING INDEX TABLESPACE fastspace WHERE (col3 IS NOT NULL)`;
await testParseAndStringify('PGTableEXConstraint', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
});
// ---------------------
// CREATE TABLE TESTS
// ---------------------
$describe('CREATE TABLE Statements', () => {
$it('should parse CREATE TABLE with various column types', async () => {
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE users (id INT)');
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE users (id INT, name VARCHAR(100))');
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE users (id INT PRIMARY KEY, email VARCHAR(255) UNIQUE)');
});
$it('should parse CREATE TABLE with table constraints', async () => {
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE users (id INT, name TEXT, CONSTRAINT pk_id PRIMARY KEY (id))');
});
$it('should parse CREATE TABLE with multiple constraints', async () => {
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE users (id INT, name TEXT, PRIMARY KEY (id), UNIQUE (name))');
});
$it('should parse CREATE TABLE with all constraint types', async () => {
const sql =
`CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(255) UNIQUE,
age INT CHECK (age > 0),
group_id INT,
CONSTRAINT fk_group FOREIGN KEY (group_id) REFERENCES tbl_groups (id),
CONSTRAINT email_unique UNIQUE NULLS DISTINCT (email),
CONSTRAINT exclude_circle EXCLUDE USING GIST (circle_col WITH &&) WHERE (circle_col IS NOT NULL)
)`;
await testParseAndStringify('CreateTableStmt', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with composite primary and unique keys', async () => {
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE orders (order_id INT, product_id INT, PRIMARY KEY (order_id, product_id), UNIQUE (product_id))');
});
$it('should parse CREATE TABLE with multiple foreign keys', async () => {
const sql =
`CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT,
product_id INT,
CONSTRAINT fk_order FOREIGN KEY (order_id) REFERENCES orders (id),
CONSTRAINT fk_product FOREIGN KEY (product_id) REFERENCES products (id)
)`;
await testParseAndStringify('CreateTableStmt', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with CHECK constraints and expressions', async () => {
const sql =
`CREATE TABLE employees (
id INT PRIMARY KEY,
salary DECIMAL(10, 2),
CHECK (salary > 0),
CHECK (salary < 100000)
)`;
await testParseAndStringify('CreateTableStmt', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with DEFERRABLE constraints', async () => {
const sql =
`CREATE TABLE payments (
id INT PRIMARY KEY,
user_id INT,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'postgres', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with all constraint clauses and options', async () => {
const sql =
`CREATE TABLE inventory (
id INT GENERATED ALWAYS AS IDENTITY,
sku VARCHAR(50) NOT NULL UNIQUE,
quantity INT DEFAULT 0 CHECK (quantity >= 0),
price DECIMAL(10, 2) DEFAULT 0.00,
location_id INT,
CONSTRAINT pk_inventory PRIMARY KEY (id),
CONSTRAINT fk_location FOREIGN KEY (location_id) REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT uq_sku UNIQUE NULLS DISTINCT (sku)
)`;
await testParseAndStringify('CreateTableStmt', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with quoted identifiers and reserved words', async () => {
const sql =
`CREATE TABLE "select" (
"from" INT PRIMARY KEY,
"to" VARCHAR(100) NOT NULL,
"order" INT
)`;
await testParseAndStringify('CreateTableStmt', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with column comments and table comments', async () => {
const sql =
`CREATE TABLE products (
id INT PRIMARY KEY COMMENT 'Product ID',
name VARCHAR(100) COMMENT 'Product name',
price DECIMAL(10, 2) COMMENT 'Product price'
) COMMENT = 'Products table'`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with VISIBLE and INVISIBLE column modifiers (MySQL)', async () => {
const sql =
`CREATE TABLE t1 (
a INT VISIBLE,
b INT INVISIBLE,
c VARCHAR(100) INVISIBLE
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with generated columns (MySQL)', async () => {
const sql =
`CREATE TABLE t1 (
a INT,
b INT GENERATED ALWAYS AS (a + 1) STORED,
c INT GENERATED ALWAYS AS (a * 2) VIRTUAL
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with ENUM and SET types (MySQL)', async () => {
const sql =
`CREATE TABLE enums (
status ENUM('active', 'inactive', 'pending'),
flags SET('a', 'b', 'c')
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with composite foreign keys and actions', async () => {
const sql =
`CREATE TABLE child (
id INT,
parent_id1 INT,
parent_id2 INT,
CONSTRAINT fk_parent FOREIGN KEY (parent_id1, parent_id2) REFERENCES parent (id1, id2) ON DELETE CASCADE ON UPDATE SET NULL
)`;
await testParseAndStringify('CreateTableStmt', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with multiple constraints and mixed syntax', async () => {
const sql =
`CREATE TABLE test_mix (
id INT PRIMARY KEY,
code VARCHAR(10) UNIQUE,
value INT,
CONSTRAINT chk_value CHECK (value > 0),
UNIQUE (code, value),
FOREIGN KEY (value) REFERENCES ref_table (ref_col)
)`;
await testParseAndStringify('CreateTableStmt', sql, { prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with default expressions and functions', async () => {
const sql =
`CREATE TABLE logs (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT NOW()
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'postgres', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with array types and constraints', async () => {
const sql =
`CREATE TABLE arr_test (
id INT PRIMARY KEY,
tags TEXT[],
scores INT[] CHECK (array_length(scores, 1) > 0)
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'postgres', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with identity columns and sequences', async () => {
const sql =
`CREATE TABLE seq_test (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
val INT
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'postgres', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with partitioning (Postgres)', async () => {
const sql =
`CREATE TABLE measurement (
city_id INT NOT NULL,
logdate DATE NOT NULL,
peaktemp INT,
unitsales INT
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'postgres', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with character set (MySQL)', async () => {
const sql =
`CREATE TABLE collate_test (
name VARCHAR(100)
) ENGINE = InnoDB DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with schema-qualified name and options', async () => {
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE public.users (id INT)');
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE IF NOT EXISTS users (id INT)');
await testParseAndStringify('CreateTableStmt', 'CREATE TEMPORARY TABLE temp_users (id INT)');
await testParseAndStringify('CreateTableStmt', 'CREATE TABLE users (id INT) ENGINE = InnoDB DEFAULT CHARSET = utf8', { dialect: 'mysql' });
});
});
// ---------------------
// MySQL-specific features
// ---------------------
$describe('MySQL Specific Constraints', () => {
$it('should parse AUTO_INCREMENT', async () => {
await testParseAndStringify('MYColumnAutoIncrementModifier', 'AUTO_INCREMENT', { dialect: 'mysql' });
});
$it('should parse MySQL column expressions and virtual columns', async () => {
await testParseAndStringify('ColumnExpressionConstraint', 'GENERATED ALWAYS AS (LOWER(name))', { dialect: 'mysql' });
await testParseAndStringify('ColumnExpressionConstraint', 'GENERATED ALWAYS AS (LOWER(name)) STORED', { dialect: 'mysql' });
await testParseAndStringify('ColumnExpressionConstraint', 'GENERATED ALWAYS AS (LOWER(name)) VIRTUAL', { dialect: 'mysql' });
await testParseAndStringify('ColumnExpressionConstraint', 'AS (col1 * 2)', { dialect: 'mysql' });
});
$it('should parse ON UPDATE CURRENT_TIMESTAMP (MySQL)', async () => {
await testParseAndStringify('MYColumnOnUpdateModifier', 'ON UPDATE CURRENT_TIMESTAMP', { dialect: 'mysql' });
});
$it('should parse CREATE TABLE with AUTO_INCREMENT (MySQL)', async () => {
const sql =
`CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with generated columns and VIRTUAL/STORED (MySQL)', async () => {
const sql =
`CREATE TABLE t1 (
a INT,
b INT GENERATED ALWAYS AS (a + 1) STORED,
c INT GENERATED ALWAYS AS (a * 2) VIRTUAL
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with AS expression (MySQL)', async () => {
const sql =
`CREATE TABLE t2 (
a INT,
b INT AS (a * 2)
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
$it('should parse CREATE TABLE with ON UPDATE CURRENT_TIMESTAMP (MySQL)', async () => {
const sql =
`CREATE TABLE logs (
id INT PRIMARY KEY AUTO_INCREMENT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)`;
await testParseAndStringify('CreateTableStmt', sql, { dialect: 'mysql', prettyPrint: true, autoLineBreakThreshold: 5 });
});
});
});