Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions parser_pg/src/translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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}"))
Expand Down Expand Up @@ -3545,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,
Expand All @@ -3563,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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -3718,7 +3725,8 @@ pub fn map_pg_type(pg_type: &str, params: &[i64]) -> Option<PgTypeMapping> {
}

// 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(),
Expand Down
8 changes: 3 additions & 5 deletions tests/integration/postgres/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
};
Expand All @@ -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)");
Expand Down
1 change: 1 addition & 0 deletions tests/integration/postgres/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ mod dialect;
mod domain;
mod parse_edge_cases;
mod table;
mod type_aliases;
mod update_from;
73 changes: 73 additions & 0 deletions tests/integration/postgres/type_aliases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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);
}
15 changes: 10 additions & 5 deletions tests/integration/postgres/update_from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ fn setup_tables(conn: &Arc<turso_core::Connection>) {
.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<turso_core::Connection>, sql: &str) -> Vec<i64> {
Expand Down
Loading