feat(pg-from): constrains
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
use rusqlite::{Connection, Result};
|
use rusqlite::{Connection, Result};
|
||||||
use rusqlite::types::ValueRef;
|
use rusqlite::types::ValueRef;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -7,6 +8,7 @@ use std::fs::File;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
/// Print help/usage information.
|
||||||
fn print_help(program: &str) {
|
fn print_help(program: &str) {
|
||||||
println!(
|
println!(
|
||||||
"Usage: {} <sqlite_file> <output_sql_file> <postgres_schema> [--inherit=<inherit_clause> ...]\n\n\
|
"Usage: {} <sqlite_file> <output_sql_file> <postgres_schema> [--inherit=<inherit_clause> ...]\n\n\
|
||||||
@@ -16,9 +18,11 @@ fn print_help(program: &str) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Structure representing one column from PRAGMA table_info.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ColumnInfo {
|
struct ColumnInfo {
|
||||||
_cid: i32,
|
#[allow(dead_code)]
|
||||||
|
cid: i32,
|
||||||
name: String,
|
name: String,
|
||||||
data_type: String,
|
data_type: String,
|
||||||
notnull: bool,
|
notnull: bool,
|
||||||
@@ -26,6 +30,7 @@ struct ColumnInfo {
|
|||||||
pk: i32,
|
pk: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts an SQLite type to a PostgreSQL type (very simple logic).
|
||||||
fn convert_sqlite_type_to_postgres(sqlite_type: &str) -> String {
|
fn convert_sqlite_type_to_postgres(sqlite_type: &str) -> String {
|
||||||
let upper = sqlite_type.to_uppercase();
|
let upper = sqlite_type.to_uppercase();
|
||||||
if upper.contains("INT") {
|
if upper.contains("INT") {
|
||||||
@@ -41,6 +46,8 @@ fn convert_sqlite_type_to_postgres(sqlite_type: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates the CREATE TABLE statement for a given table, based on PRAGMA table_info.
|
||||||
|
/// If an inheritance clause is provided, appends INHERITS (<clause>).
|
||||||
fn generate_create_table_sql(
|
fn generate_create_table_sql(
|
||||||
table: &str,
|
table: &str,
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
@@ -51,7 +58,7 @@ fn generate_create_table_sql(
|
|||||||
let columns: Vec<ColumnInfo> = stmt
|
let columns: Vec<ColumnInfo> = stmt
|
||||||
.query_map([], |row| {
|
.query_map([], |row| {
|
||||||
Ok(ColumnInfo {
|
Ok(ColumnInfo {
|
||||||
_cid: row.get(0)?,
|
cid: row.get(0)?,
|
||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
data_type: row.get(2)?,
|
data_type: row.get(2)?,
|
||||||
notnull: row.get::<_, i32>(3)? != 0,
|
notnull: row.get::<_, i32>(3)? != 0,
|
||||||
@@ -64,6 +71,7 @@ fn generate_create_table_sql(
|
|||||||
let mut column_defs = Vec::new();
|
let mut column_defs = Vec::new();
|
||||||
let pk_columns: Vec<&ColumnInfo> = columns.iter().filter(|col| col.pk > 0).collect();
|
let pk_columns: Vec<&ColumnInfo> = columns.iter().filter(|col| col.pk > 0).collect();
|
||||||
|
|
||||||
|
// If exactly one PK and its type starts with "INTEGER", generate SERIAL.
|
||||||
let single_autoinc = if pk_columns.len() == 1 {
|
let single_autoinc = if pk_columns.len() == 1 {
|
||||||
let col = pk_columns[0];
|
let col = pk_columns[0];
|
||||||
col.data_type.to_uppercase().starts_with("INTEGER")
|
col.data_type.to_uppercase().starts_with("INTEGER")
|
||||||
@@ -92,6 +100,7 @@ fn generate_create_table_sql(
|
|||||||
column_defs.push(col_def);
|
column_defs.push(col_def);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If composite primary key exists, add it as a separate constraint.
|
||||||
if pk_columns.len() > 1 {
|
if pk_columns.len() > 1 {
|
||||||
let pk_names: Vec<String> = pk_columns
|
let pk_names: Vec<String> = pk_columns
|
||||||
.iter()
|
.iter()
|
||||||
@@ -114,6 +123,7 @@ fn generate_create_table_sql(
|
|||||||
Ok(table_sql)
|
Ok(table_sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates DDL for indexes of a given table.
|
||||||
fn generate_indexes_sql(table: &str, conn: &Connection, schema: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
fn generate_indexes_sql(table: &str, conn: &Connection, schema: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
let mut indexes = Vec::new();
|
let mut indexes = Vec::new();
|
||||||
let mut stmt = conn.prepare(&format!("PRAGMA index_list(\"{}\")", table))?;
|
let mut stmt = conn.prepare(&format!("PRAGMA index_list(\"{}\")", table))?;
|
||||||
@@ -153,10 +163,87 @@ fn generate_indexes_sql(table: &str, conn: &Connection, schema: &str) -> Result<
|
|||||||
Ok(indexes)
|
Ok(indexes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents one foreign key entry from PRAGMA foreign_key_list.
|
||||||
|
struct ForeignKeyInfo {
|
||||||
|
id: i32,
|
||||||
|
seq: i32,
|
||||||
|
table: String,
|
||||||
|
from: String,
|
||||||
|
to: String,
|
||||||
|
on_update: String,
|
||||||
|
on_delete: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
r#match: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates foreign key constraints for the given table. It groups rows by foreign key ID
|
||||||
|
/// (to support multi‑column foreign keys) and produces ALTER TABLE … ADD CONSTRAINT statements.
|
||||||
|
fn generate_foreign_keys_sql(
|
||||||
|
table: &str,
|
||||||
|
conn: &Connection,
|
||||||
|
schema: &str,
|
||||||
|
) -> Result<Vec<String>, Box<dyn Error>> {
|
||||||
|
let mut stmt = conn.prepare(&format!("PRAGMA foreign_key_list(\"{}\")", table))?;
|
||||||
|
let fk_rows: Vec<ForeignKeyInfo> = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok(ForeignKeyInfo {
|
||||||
|
id: row.get(0)?,
|
||||||
|
seq: row.get(1)?,
|
||||||
|
table: row.get(2)?,
|
||||||
|
from: row.get(3)?,
|
||||||
|
to: row.get(4)?,
|
||||||
|
on_update: row.get(5)?,
|
||||||
|
on_delete: row.get(6)?,
|
||||||
|
r#match: row.get(7)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
// Group rows by foreign key ID.
|
||||||
|
let mut fk_map: HashMap<i32, Vec<ForeignKeyInfo>> = HashMap::new();
|
||||||
|
for fk in fk_rows {
|
||||||
|
fk_map.entry(fk.id).or_default().push(fk);
|
||||||
|
}
|
||||||
|
let mut constraints = Vec::new();
|
||||||
|
for (fk_id, mut fks) in fk_map {
|
||||||
|
// Sort by sequence number.
|
||||||
|
fks.sort_by_key(|fk| fk.seq);
|
||||||
|
// All entries in this group refer to the same target table.
|
||||||
|
let ref_table = &fks[0].table;
|
||||||
|
let on_update = &fks[0].on_update;
|
||||||
|
let on_delete = &fks[0].on_delete;
|
||||||
|
let from_columns: Vec<String> = fks.iter().map(|fk| format!("\"{}\"", fk.from)).collect();
|
||||||
|
let to_columns: Vec<String> = fks.iter().map(|fk| format!("\"{}\"", fk.to)).collect();
|
||||||
|
// Generate a constraint name, e.g. fk_table_1.
|
||||||
|
let constraint_name = format!("fk_{}_{}", table, fk_id);
|
||||||
|
let mut constraint = format!(
|
||||||
|
"ALTER TABLE {}.\"{}\" ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}.\"{}\" ({})",
|
||||||
|
schema,
|
||||||
|
table,
|
||||||
|
constraint_name,
|
||||||
|
from_columns.join(", "),
|
||||||
|
schema,
|
||||||
|
ref_table,
|
||||||
|
to_columns.join(", ")
|
||||||
|
);
|
||||||
|
if !on_update.is_empty() && on_update.to_uppercase() != "NO ACTION" {
|
||||||
|
constraint.push_str(&format!(" ON UPDATE {}", on_update));
|
||||||
|
}
|
||||||
|
if !on_delete.is_empty() && on_delete.to_uppercase() != "NO ACTION" {
|
||||||
|
constraint.push_str(&format!(" ON DELETE {}", on_delete));
|
||||||
|
}
|
||||||
|
constraint.push(';');
|
||||||
|
constraints.push(constraint);
|
||||||
|
}
|
||||||
|
Ok(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Escapes a text value for the PostgreSQL COPY format (e.g. escapes backslashes).
|
||||||
fn escape_copy_text(s: &str) -> String {
|
fn escape_copy_text(s: &str) -> String {
|
||||||
s.replace("\\", "\\\\")
|
s.replace("\\", "\\\\")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formats a single column value for the PostgreSQL COPY command. NULL values become "\N".
|
||||||
fn format_copy_field(value: ValueRef) -> String {
|
fn format_copy_field(value: ValueRef) -> String {
|
||||||
match value {
|
match value {
|
||||||
ValueRef::Null => "\\N".to_string(),
|
ValueRef::Null => "\\N".to_string(),
|
||||||
@@ -167,44 +254,52 @@ fn format_copy_field(value: ValueRef) -> String {
|
|||||||
escape_copy_text(s)
|
escape_copy_text(s)
|
||||||
},
|
},
|
||||||
ValueRef::Blob(b) => {
|
ValueRef::Blob(b) => {
|
||||||
|
// Convert blob to a hex string prefixed with \x.
|
||||||
let hex: String = b.iter().map(|byte| format!("{:02X}", byte)).collect();
|
let hex: String = b.iter().map(|byte| format!("{:02X}", byte)).collect();
|
||||||
format!("\\x{}", hex)
|
format!("\\x{}", hex)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dumps data for a table in PostgreSQL COPY format.
|
||||||
fn dump_table_data(table: &str, conn: &Connection, schema: &str, out: &mut File) -> Result<(), Box<dyn Error>> {
|
fn dump_table_data(table: &str, conn: &Connection, schema: &str, out: &mut File) -> Result<(), Box<dyn Error>> {
|
||||||
|
// Get column names using PRAGMA table_info.
|
||||||
let mut stmt = conn.prepare(&format!("PRAGMA table_info(\"{}\")", table))?;
|
let mut stmt = conn.prepare(&format!("PRAGMA table_info(\"{}\")", table))?;
|
||||||
let column_names: Result<Vec<String>, _> =
|
let column_names: Result<Vec<String>, _> = stmt.query_map([], |row| row.get(1))?.collect();
|
||||||
stmt.query_map([], |row| row.get(1))?.collect();
|
|
||||||
let column_names = column_names?;
|
let column_names = column_names?;
|
||||||
|
|
||||||
|
// Write COPY header.
|
||||||
writeln!(out, "\n-- Data for table {}", table)?;
|
writeln!(out, "\n-- Data for table {}", table)?;
|
||||||
writeln!(out, "COPY {}.\"{}\" ({}) FROM stdin;", schema, table, column_names.join(", "))?;
|
writeln!(out, "COPY {}.\"{}\" ({}) FROM stdin;", schema, table, column_names.join(", "))?;
|
||||||
|
|
||||||
|
// Query all rows from the table.
|
||||||
let mut stmt = conn.prepare(&format!("SELECT * FROM \"{}\"", table))?;
|
let mut stmt = conn.prepare(&format!("SELECT * FROM \"{}\"", table))?;
|
||||||
let mut rows = stmt.query([])?;
|
let mut rows = stmt.query([])?;
|
||||||
while let Some(row) = rows.next()? {
|
// Use the known number of columns.
|
||||||
let col_count = column_names.len();
|
let col_count = column_names.len();
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
let mut fields = Vec::new();
|
let mut fields = Vec::new();
|
||||||
for i in 0..col_count {
|
for i in 0..col_count {
|
||||||
let value = row.get_ref(i)?;
|
let value = row.get_ref(i)?;
|
||||||
fields.push(format_copy_field(value));
|
fields.push(format_copy_field(value));
|
||||||
}
|
}
|
||||||
|
// Write tab-separated fields.
|
||||||
writeln!(out, "{}", fields.join("\t"))?;
|
writeln!(out, "{}", fields.join("\t"))?;
|
||||||
}
|
}
|
||||||
|
// End COPY command.
|
||||||
writeln!(out, "\\.")?;
|
writeln!(out, "\\.")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
// Process command-line arguments.
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||||
print_help(&args[0]);
|
print_help(&args[0]);
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
if args.len() < 4 {
|
if args.len() < 4 {
|
||||||
eprintln!("Error: Недостаточно аргументов.\n");
|
eprintln!("Error: Not enough arguments.\n");
|
||||||
print_help(&args[0]);
|
print_help(&args[0]);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
@@ -212,6 +307,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let output_file = &args[2];
|
let output_file = &args[2];
|
||||||
let schema = &args[3];
|
let schema = &args[3];
|
||||||
|
|
||||||
|
// Gather multiple --inherit options.
|
||||||
let mut inherit_clauses: Vec<String> = Vec::new();
|
let mut inherit_clauses: Vec<String> = Vec::new();
|
||||||
for arg in &args[4..] {
|
for arg in &args[4..] {
|
||||||
if arg.starts_with("--inherit=") {
|
if arg.starts_with("--inherit=") {
|
||||||
@@ -224,16 +320,20 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
Some(inherit_clauses.join(", "))
|
Some(inherit_clauses.join(", "))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Copy the original SQLite file into a temporary file.
|
||||||
let temp_file = NamedTempFile::new()?;
|
let temp_file = NamedTempFile::new()?;
|
||||||
fs::copy(sqlite_file, temp_file.path())?;
|
fs::copy(sqlite_file, temp_file.path())?;
|
||||||
let conn = Connection::open(temp_file.path())?;
|
let conn = Connection::open(temp_file.path())?;
|
||||||
|
|
||||||
|
// Open (or create) the output file.
|
||||||
let mut out = File::create(output_file)?;
|
let mut out = File::create(output_file)?;
|
||||||
|
|
||||||
|
// Write header.
|
||||||
writeln!(out, "-- PostgreSQL database dump generated from SQLite")?;
|
writeln!(out, "-- PostgreSQL database dump generated from SQLite")?;
|
||||||
writeln!(out, "CREATE SCHEMA IF NOT EXISTS {};\n", schema)?;
|
writeln!(out, "CREATE SCHEMA IF NOT EXISTS {};\n", schema)?;
|
||||||
writeln!(out, "SET client_encoding = 'UTF8';\n")?;
|
writeln!(out, "SET client_encoding = 'UTF8';\n")?;
|
||||||
|
|
||||||
|
// Get table names (excluding internal SQLite tables).
|
||||||
let mut table_names = Vec::new();
|
let mut table_names = Vec::new();
|
||||||
{
|
{
|
||||||
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")?;
|
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")?;
|
||||||
@@ -244,6 +344,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate DDL for each table, its indexes, and foreign key constraints.
|
||||||
for table in &table_names {
|
for table in &table_names {
|
||||||
let table_sql = generate_create_table_sql(table, &conn, schema, inherit_clause.as_deref())?;
|
let table_sql = generate_create_table_sql(table, &conn, schema, inherit_clause.as_deref())?;
|
||||||
writeln!(out, "{}\n", table_sql)?;
|
writeln!(out, "{}\n", table_sql)?;
|
||||||
@@ -253,8 +354,13 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
for idx in indexes {
|
for idx in indexes {
|
||||||
writeln!(out, "{}\n", idx)?;
|
writeln!(out, "{}\n", idx)?;
|
||||||
}
|
}
|
||||||
|
let fkeys = generate_foreign_keys_sql(table, &conn, schema)?;
|
||||||
|
for fk in fkeys {
|
||||||
|
writeln!(out, "{}\n", fk)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process sqlite_sequence (for autoincrement values).
|
||||||
let sqlite_sequence_exists: bool = conn.query_row(
|
let sqlite_sequence_exists: bool = conn.query_row(
|
||||||
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='sqlite_sequence')",
|
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='sqlite_sequence')",
|
||||||
[],
|
[],
|
||||||
@@ -279,6 +385,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dump data for each table.
|
||||||
for table in &table_names {
|
for table in &table_names {
|
||||||
dump_table_data(table, &conn, schema, &mut out)?;
|
dump_table_data(table, &conn, schema, &mut out)?;
|
||||||
}
|
}
|
||||||
|
|||||||
76
package/postgres/pg-from/test/fixture/expected.sql
Normal file
76
package/postgres/pg-from/test/fixture/expected.sql
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
-- PostgreSQL database dump generated from SQLite
|
||||||
|
CREATE SCHEMA IF NOT EXISTS legacy;
|
||||||
|
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
|
||||||
|
CREATE TABLE legacy."authors" (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"email" text
|
||||||
|
) INHERITS (created_at, updated_at);
|
||||||
|
|
||||||
|
ALTER TABLE legacy."authors" OWNER TO postgres;
|
||||||
|
|
||||||
|
CREATE TABLE legacy."books" (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"author_id" bigint NOT NULL,
|
||||||
|
"published_date" text,
|
||||||
|
"price" double precision
|
||||||
|
) INHERITS (created_at, updated_at);
|
||||||
|
|
||||||
|
ALTER TABLE legacy."books" OWNER TO postgres;
|
||||||
|
|
||||||
|
CREATE INDEX idx_books_price ON legacy."books" ("price");
|
||||||
|
|
||||||
|
CREATE INDEX idx_books_title ON legacy."books" ("title");
|
||||||
|
|
||||||
|
ALTER TABLE legacy."books" ADD CONSTRAINT fk_books_0 FOREIGN KEY ("author_id") REFERENCES legacy."authors" ("id");
|
||||||
|
|
||||||
|
CREATE TABLE legacy."reviews" (
|
||||||
|
"book_id" bigint,
|
||||||
|
"review_id" bigint,
|
||||||
|
"reviewer" text,
|
||||||
|
"rating" bigint,
|
||||||
|
"comment" text,
|
||||||
|
PRIMARY KEY ("book_id", "review_id")
|
||||||
|
) INHERITS (created_at, updated_at);
|
||||||
|
|
||||||
|
ALTER TABLE legacy."reviews" OWNER TO postgres;
|
||||||
|
|
||||||
|
ALTER TABLE legacy."reviews" ADD CONSTRAINT fk_reviews_0 FOREIGN KEY ("book_id") REFERENCES legacy."books" ("id");
|
||||||
|
|
||||||
|
CREATE TABLE legacy."book_log" (
|
||||||
|
"log_id" SERIAL PRIMARY KEY,
|
||||||
|
"book_id" bigint,
|
||||||
|
"created_at" text DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) INHERITS (created_at, updated_at);
|
||||||
|
|
||||||
|
ALTER TABLE legacy."book_log" OWNER TO postgres;
|
||||||
|
|
||||||
|
CREATE SEQUENCE legacy_books_seq START WITH 3 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
|
||||||
|
|
||||||
|
-- Data for table authors
|
||||||
|
COPY legacy."authors" (id, name, email) FROM stdin;
|
||||||
|
1 Author One author1@example.com
|
||||||
|
2 Author Two author2@example.com
|
||||||
|
\.
|
||||||
|
|
||||||
|
-- Data for table books
|
||||||
|
COPY legacy."books" (id, title, author_id, published_date, price) FROM stdin;
|
||||||
|
1 Book One 1 2020-01-01 9.99
|
||||||
|
2 Book Two 2 2021-05-15 19.99
|
||||||
|
\.
|
||||||
|
|
||||||
|
-- Data for table reviews
|
||||||
|
COPY legacy."reviews" (book_id, review_id, reviewer, rating, comment) FROM stdin;
|
||||||
|
1 1 Reviewer A 4 Good book
|
||||||
|
1 2 Reviewer B 5 Excellent!
|
||||||
|
2 1 Reviewer C 3 Average
|
||||||
|
\.
|
||||||
|
|
||||||
|
-- Data for table book_log
|
||||||
|
COPY legacy."book_log" (log_id, book_id, created_at) FROM stdin;
|
||||||
|
1 1 2025-02-04 00:21:48
|
||||||
|
2 2 2025-02-04 00:21:48
|
||||||
|
\.
|
||||||
BIN
package/postgres/pg-from/test/fixture/test.db
Normal file
BIN
package/postgres/pg-from/test/fixture/test.db
Normal file
Binary file not shown.
79
package/postgres/pg-from/test/fixture/test.sql
Normal file
79
package/postgres/pg-from/test/fixture/test.sql
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
-- Enable foreign key constraints
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Table: authors
|
||||||
|
CREATE TABLE authors (
|
||||||
|
id INTEGER PRIMARY KEY, -- Primary key using INTEGER PRIMARY KEY
|
||||||
|
name TEXT NOT NULL, -- Required field
|
||||||
|
email TEXT UNIQUE -- Unique email constraint
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table: books
|
||||||
|
CREATE TABLE books (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, -- Auto-incrementing primary key
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
author_id INTEGER NOT NULL,
|
||||||
|
published_date DATE, -- Date stored as TEXT (or ISO8601 format)
|
||||||
|
price REAL,
|
||||||
|
CONSTRAINT fk_author FOREIGN KEY(author_id) REFERENCES authors(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table: reviews with composite primary key and a CHECK constraint
|
||||||
|
CREATE TABLE reviews (
|
||||||
|
book_id INTEGER,
|
||||||
|
review_id INTEGER,
|
||||||
|
reviewer TEXT,
|
||||||
|
rating INTEGER CHECK (rating BETWEEN 1 AND 5), -- Check constraint to restrict rating values
|
||||||
|
comment TEXT,
|
||||||
|
PRIMARY KEY (book_id, review_id),
|
||||||
|
FOREIGN KEY(book_id) REFERENCES books(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create a standard index on books (non-unique)
|
||||||
|
CREATE INDEX idx_books_title ON books(title);
|
||||||
|
|
||||||
|
-- Create a partial index (only rows where price > 10)
|
||||||
|
CREATE INDEX idx_books_price ON books(price) WHERE price > 10;
|
||||||
|
|
||||||
|
-- Table: book_log for logging inserted books
|
||||||
|
CREATE TABLE book_log (
|
||||||
|
log_id INTEGER PRIMARY KEY,
|
||||||
|
book_id INTEGER,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Trigger: After inserting a book, log its id into book_log
|
||||||
|
CREATE TRIGGER trg_book_insert
|
||||||
|
AFTER INSERT ON books
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO book_log (book_id) VALUES (new.id);
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Create a view joining authors and books
|
||||||
|
CREATE VIEW vw_author_books AS
|
||||||
|
SELECT a.name AS author,
|
||||||
|
b.title,
|
||||||
|
b.published_date,
|
||||||
|
b.price
|
||||||
|
FROM authors a
|
||||||
|
JOIN books b ON a.id = b.author_id;
|
||||||
|
|
||||||
|
-- Insert sample data into authors
|
||||||
|
INSERT INTO authors (id, name, email) VALUES (1, 'Author One', 'author1@example.com');
|
||||||
|
INSERT INTO authors (name, email) VALUES ('Author Two', 'author2@example.com');
|
||||||
|
|
||||||
|
-- Insert sample data into books
|
||||||
|
INSERT INTO books (title, author_id, published_date, price) VALUES ('Book One', 1, '2020-01-01', 9.99);
|
||||||
|
INSERT INTO books (title, author_id, published_date, price) VALUES ('Book Two', 2, '2021-05-15', 19.99);
|
||||||
|
|
||||||
|
-- Insert sample data into reviews
|
||||||
|
INSERT INTO reviews (book_id, review_id, reviewer, rating, comment)
|
||||||
|
VALUES (1, 1, 'Reviewer A', 4, 'Good book');
|
||||||
|
INSERT INTO reviews (book_id, review_id, reviewer, rating, comment)
|
||||||
|
VALUES (1, 2, 'Reviewer B', 5, 'Excellent!');
|
||||||
|
INSERT INTO reviews (book_id, review_id, reviewer, rating, comment)
|
||||||
|
VALUES (2, 1, 'Reviewer C', 3, 'Average');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user