From c2a2ee15cdb477fdc09d706fa12fdfa01e7c74b8 Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Tue, 12 May 2026 17:56:14 -0700 Subject: [PATCH 1/2] fix: recognize serial2, serial4, serial8 type aliases Add support for the numeric serial aliases (serial2, serial4, serial8) that PostgreSQL accepts alongside smallserial, serial, and bigserial. Fixes https://github.com/glommer/pgmicro/issues/10 Co-Authored-By: Claude Opus 4.6 --- parser_pg/src/translator.rs | 20 ++++-- tests/integration/postgres/mod.rs | 1 + tests/integration/postgres/type_aliases.rs | 79 ++++++++++++++++++++++ 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 tests/integration/postgres/type_aliases.rs diff --git a/parser_pg/src/translator.rs b/parser_pg/src/translator.rs index f66128173..7ba60bf6b 100644 --- a/parser_pg/src/translator.rs +++ b/parser_pg/src/translator.rs @@ -151,9 +151,7 @@ impl PostgreSQLTranslator { let Some(ref inner) = elt.node else { continue }; if let Node::ColumnDef(col_def) = inner { let pg_type = extract_type_name(col_def)?; - if pg_type.eq_ignore_ascii_case("serial") - || pg_type.eq_ignore_ascii_case("bigserial") - { + if is_serial_type(&pg_type) { has_autoincrement = true; break; } @@ -281,8 +279,7 @@ impl PostgreSQLTranslator { let pg_type = extract_type_name(col_def)?; let typmods = extract_integer_typmods(col_def); - let is_serial = - pg_type.eq_ignore_ascii_case("serial") || pg_type.eq_ignore_ascii_case("bigserial"); + let is_serial = is_serial_type(&pg_type); let mapping = map_pg_type(&pg_type, &typmods).ok_or_else(|| { ParseError::ParseError(format!("unsupported PostgreSQL type: {pg_type}")) @@ -3663,6 +3660,16 @@ impl PgTypeMapping { /// PostgreSQL to Turso type mapping. /// Returns Turso custom type names (e.g. "boolean", "varchar(100)") when a +/// Returns true if the given PG type name is a serial variant (auto-incrementing integer). +/// Covers all PostgreSQL serial aliases: serial, serial2, serial4, serial8, +/// smallserial, bigserial. +fn is_serial_type(pg_type: &str) -> bool { + matches!( + pg_type.to_uppercase().as_str(), + "SERIAL" | "SERIAL2" | "SERIAL4" | "SERIAL8" | "SMALLSERIAL" | "BIGSERIAL" + ) +} + /// built-in Turso type exists, otherwise returns the base SQLite type. /// For array types (e.g. `INTEGER[]`, `_int4`), returns the base scalar type /// with `array_dimensions > 0` so native Turso arrays are used. @@ -3718,7 +3725,8 @@ pub fn map_pg_type(pg_type: &str, params: &[i64]) -> Option { } // Base types (no Turso custom type needed) - "INTEGER" | "INT" | "INT4" | "SERIAL" | "BIGSERIAL" | "SMALLSERIAL" => "INTEGER".into(), + "INTEGER" | "INT" | "INT4" | "SERIAL" | "SERIAL4" | "BIGSERIAL" | "SERIAL8" + | "SMALLSERIAL" | "SERIAL2" => "INTEGER".into(), "REAL" | "FLOAT4" | "DOUBLE PRECISION" | "FLOAT8" => "REAL".into(), "TEXT" | "BPCHAR" | "NAME" => "TEXT".into(), "BLOB" => "BLOB".into(), diff --git a/tests/integration/postgres/mod.rs b/tests/integration/postgres/mod.rs index a88e47f48..f2f810e02 100644 --- a/tests/integration/postgres/mod.rs +++ b/tests/integration/postgres/mod.rs @@ -4,4 +4,5 @@ mod dialect; mod domain; mod parse_edge_cases; mod table; +mod type_aliases; mod update_from; diff --git a/tests/integration/postgres/type_aliases.rs b/tests/integration/postgres/type_aliases.rs new file mode 100644 index 000000000..ac814dc22 --- /dev/null +++ b/tests/integration/postgres/type_aliases.rs @@ -0,0 +1,79 @@ +use crate::common::TempDatabase; +use turso_core::{Numeric, StepResult, Value}; + +/// serial2 is a PostgreSQL alias for smallserial (auto-incrementing smallint). +/// We test that the type is accepted and maps to INTEGER storage. +/// No mvcc variant: serial implies AUTOINCREMENT which is unsupported in MVCC mode. +#[turso_macros::test] +fn test_serial2_type(db: TempDatabase) { + let conn = db.connect_limbo(); + conn.execute("PRAGMA sql_dialect = postgres").unwrap(); + + conn.execute("CREATE TABLE t_serial2(id serial2, name TEXT)") + .unwrap(); + conn.execute("INSERT INTO t_serial2(id, name) VALUES (1, 'alice')") + .unwrap(); + + let mut rows = conn + .query("SELECT id, name FROM t_serial2") + .unwrap() + .unwrap(); + let StepResult::Row = rows.step().unwrap() else { + panic!("expected row"); + }; + let row = rows.row().unwrap(); + let Value::Numeric(Numeric::Integer(id)) = row.get_value(0) else { + panic!("expected integer id"); + }; + assert_eq!(*id, 1); +} + +/// serial4 is a PostgreSQL alias for serial (auto-incrementing integer). +#[turso_macros::test] +fn test_serial4_type(db: TempDatabase) { + let conn = db.connect_limbo(); + conn.execute("PRAGMA sql_dialect = postgres").unwrap(); + + conn.execute("CREATE TABLE t_serial4(id serial4, name TEXT)") + .unwrap(); + conn.execute("INSERT INTO t_serial4(id, name) VALUES (1, 'alice')") + .unwrap(); + + let mut rows = conn + .query("SELECT id FROM t_serial4") + .unwrap() + .unwrap(); + let StepResult::Row = rows.step().unwrap() else { + panic!("expected row"); + }; + let row = rows.row().unwrap(); + let Value::Numeric(Numeric::Integer(id)) = row.get_value(0) else { + panic!("expected integer id"); + }; + assert_eq!(*id, 1); +} + +/// serial8 is a PostgreSQL alias for bigserial (auto-incrementing bigint). +#[turso_macros::test] +fn test_serial8_type(db: TempDatabase) { + let conn = db.connect_limbo(); + conn.execute("PRAGMA sql_dialect = postgres").unwrap(); + + conn.execute("CREATE TABLE t_serial8(id serial8, name TEXT)") + .unwrap(); + conn.execute("INSERT INTO t_serial8(id, name) VALUES (1, 'alice')") + .unwrap(); + + let mut rows = conn + .query("SELECT id FROM t_serial8") + .unwrap() + .unwrap(); + let StepResult::Row = rows.step().unwrap() else { + panic!("expected row"); + }; + let row = rows.row().unwrap(); + let Value::Numeric(Numeric::Integer(id)) = row.get_value(0) else { + panic!("expected integer id"); + }; + assert_eq!(*id, 1); +} From a71beb54453ffcfe059559e504331d3a807b2ec3 Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Tue, 12 May 2026 18:09:30 -0700 Subject: [PATCH 2/2] style: fix formatting issues caught by CI Co-Authored-By: Claude Opus 4.6 --- parser_pg/src/translator.rs | 10 +++++----- tests/integration/postgres/domain.rs | 8 +++----- tests/integration/postgres/type_aliases.rs | 10 ++-------- tests/integration/postgres/update_from.rs | 15 ++++++++++----- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/parser_pg/src/translator.rs b/parser_pg/src/translator.rs index 7ba60bf6b..ad3b27ca8 100644 --- a/parser_pg/src/translator.rs +++ b/parser_pg/src/translator.rs @@ -3542,9 +3542,10 @@ impl PostgreSQLTranslator { } // Extract base type from type_name and map PG type names to Turso names - let type_name_node = domain.type_name.as_ref().ok_or_else(|| { - ParseError::ParseError("CREATE DOMAIN missing base type".into()) - })?; + let type_name_node = domain + .type_name + .as_ref() + .ok_or_else(|| ParseError::ParseError("CREATE DOMAIN missing base type".into()))?; let pg_type = extract_type_name_from_typename(type_name_node)?; let base_type = match map_pg_type(&pg_type, &[]) { Some(mapping) => mapping.type_name, @@ -3560,8 +3561,7 @@ impl PostgreSQLTranslator { let Some(Node::Constraint(constraint)) = &constraint_node.node else { continue; }; - let contype = - ConstrType::try_from(constraint.contype).unwrap_or(ConstrType::Undefined); + let contype = ConstrType::try_from(constraint.contype).unwrap_or(ConstrType::Undefined); match contype { ConstrType::ConstrDefault => { if let Some(ref raw_expr) = constraint.raw_expr { diff --git a/tests/integration/postgres/domain.rs b/tests/integration/postgres/domain.rs index c8d3f7deb..8a229b3f6 100644 --- a/tests/integration/postgres/domain.rs +++ b/tests/integration/postgres/domain.rs @@ -40,10 +40,7 @@ fn test_pg_create_domain_text_with_check(db: TempDatabase) { // Valid conn.execute("INSERT INTO contacts (addr) VALUES ('user@example.com')") .unwrap(); - let mut rows = conn - .query("SELECT addr FROM contacts") - .unwrap() - .unwrap(); + let mut rows = conn.query("SELECT addr FROM contacts").unwrap().unwrap(); let StepResult::Row = rows.step().unwrap() else { panic!("expected row"); }; @@ -69,7 +66,8 @@ fn test_pg_create_domain_not_null(db: TempDatabase) { conn.execute("CREATE TABLE t (val nn_text)").unwrap(); // Valid - conn.execute("INSERT INTO t (val) VALUES ('hello')").unwrap(); + conn.execute("INSERT INTO t (val) VALUES ('hello')") + .unwrap(); // NULL should fail let result = conn.execute("INSERT INTO t (val) VALUES (NULL)"); diff --git a/tests/integration/postgres/type_aliases.rs b/tests/integration/postgres/type_aliases.rs index ac814dc22..e25bf5e3c 100644 --- a/tests/integration/postgres/type_aliases.rs +++ b/tests/integration/postgres/type_aliases.rs @@ -39,10 +39,7 @@ fn test_serial4_type(db: TempDatabase) { conn.execute("INSERT INTO t_serial4(id, name) VALUES (1, 'alice')") .unwrap(); - let mut rows = conn - .query("SELECT id FROM t_serial4") - .unwrap() - .unwrap(); + let mut rows = conn.query("SELECT id FROM t_serial4").unwrap().unwrap(); let StepResult::Row = rows.step().unwrap() else { panic!("expected row"); }; @@ -64,10 +61,7 @@ fn test_serial8_type(db: TempDatabase) { conn.execute("INSERT INTO t_serial8(id, name) VALUES (1, 'alice')") .unwrap(); - let mut rows = conn - .query("SELECT id FROM t_serial8") - .unwrap() - .unwrap(); + let mut rows = conn.query("SELECT id FROM t_serial8").unwrap().unwrap(); let StepResult::Row = rows.step().unwrap() else { panic!("expected row"); }; diff --git a/tests/integration/postgres/update_from.rs b/tests/integration/postgres/update_from.rs index 248f361de..d1f7fe6da 100644 --- a/tests/integration/postgres/update_from.rs +++ b/tests/integration/postgres/update_from.rs @@ -9,11 +9,16 @@ fn setup_tables(conn: &Arc) { .unwrap(); conn.execute("CREATE TABLE t2 (id integer PRIMARY KEY, x integer, y text)") .unwrap(); - conn.execute("INSERT INTO t1 VALUES (1, 10, 'one')").unwrap(); - conn.execute("INSERT INTO t1 VALUES (2, 20, 'two')").unwrap(); - conn.execute("INSERT INTO t1 VALUES (3, 30, 'three')").unwrap(); - conn.execute("INSERT INTO t2 VALUES (1, 100, 'alpha')").unwrap(); - conn.execute("INSERT INTO t2 VALUES (2, 200, 'beta')").unwrap(); + conn.execute("INSERT INTO t1 VALUES (1, 10, 'one')") + .unwrap(); + conn.execute("INSERT INTO t1 VALUES (2, 20, 'two')") + .unwrap(); + conn.execute("INSERT INTO t1 VALUES (3, 30, 'three')") + .unwrap(); + conn.execute("INSERT INTO t2 VALUES (1, 100, 'alpha')") + .unwrap(); + conn.execute("INSERT INTO t2 VALUES (2, 200, 'beta')") + .unwrap(); } fn query_integer(conn: &Arc, sql: &str) -> Vec {