Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
67d668b
feat(linux): export a database to a SQL file and import SQL files
datlechin Nov 5, 2026
1c7e504
docs(linux): add the changelog fragment for database export and SQL i…
datlechin Nov 5, 2026
15707a0
fix(core): address the first review of the dump and the SQL file import
datlechin Nov 5, 2026
0729a4d
fix(core): keep imports inside their transaction and dumps free of re…
datlechin Nov 5, 2026
bf29230
fix(core): order dumps by what objects name and report what a dump le…
datlechin Nov 5, 2026
cdb32b9
fix(core): write rows of computed columns, order pre-data objects and…
datlechin Nov 5, 2026
f6ed54e
fix(core): order a dump's objects across kinds and keep SQLite foreig…
datlechin Nov 5, 2026
8a72d47
fix(core): drop the source database from stored programs and make wha…
datlechin Nov 5, 2026
581a9bd
fix(linux): read the import file's size through GIO and open it on th…
datlechin Nov 5, 2026
886a77d
fix(core): order what any pre-data object names, write period times a…
datlechin Nov 5, 2026
2f4f394
fix(core): write PostgreSQL collations, read the session's escape mod…
datlechin Nov 5, 2026
34b6a60
fix(core): leave common table expressions and doubled quotes out of d…
datlechin Nov 5, 2026
828d605
fix(core): make PostgreSQL sequences before domains, keep MySQL view …
datlechin Nov 5, 2026
edbdc50
fix(core): keep SQL Server collations, report filtered SQL Server tab…
datlechin Nov 5, 2026
d16f009
fix(core): stop an import whose SQL Server code closed its transactio…
datlechin Nov 5, 2026
20f5193
fix(core): script SQL Server modules under their current names and re…
datlechin Nov 5, 2026
7e7f027
fix(core): leave materialized views empty in a dump without rows, rep…
datlechin Nov 5, 2026
3bc6d5c
fix(core): stop reading a file once the import ends, report an unclea…
datlechin Nov 5, 2026
0c5a6dc
fix(core): compare partition expressions and keep defaults set on Pos…
datlechin Nov 5, 2026
c9377d2
docs(linux): list extension configuration rows among what a PostgreSQ…
datlechin Nov 5, 2026
dd0cd5f
fix(core): split the SQL import and its dialog by step and clear the …
datlechin Nov 5, 2026
58eea92
fix(core): pad wide test rows by hand, expect the GO split skip and l…
datlechin Nov 5, 2026
f4a8784
fix(linux): show file names, statements and CSV column names as writt…
datlechin Nov 5, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ async-trait.workspace = true
chrono.workspace = true
comfy-table.workspace = true
csv.workspace = true
flate2.workspace = true
futures.workspace = true
num-bigint.workspace = true
rust_xlsxwriter.workspace = true
Expand Down
10 changes: 10 additions & 0 deletions core/crates/core/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::column::ColumnInfo;
use crate::commit::{CommitResolution, CommitToken};
use crate::ddl::{CharacterSet, InboundForeignKey, OutboundForeignKey, TableDefinition};
use crate::dialect::SqlDialect;
use crate::dump::{DumpCatalog, DumpScope};
use crate::editor_session::{EditorSession, EditorSessionOptions};
use crate::error::DriverError;
use crate::foreign_key_info::ForeignKeyInfo;
Expand Down Expand Up @@ -118,6 +119,15 @@ pub trait Connection: Send + Sync {
})
}

/// What a dump of `scope` writes: each object's SQL in the server's
/// own text where the engine keeps one, and the tables whose rows go
/// in. Read as one operation, so it has the call's whole deadline.
async fn read_dump_catalog(&self, _scope: &DumpScope, _options: CallOptions) -> Result<DumpCatalog, DriverError> {
Err(DriverError::Unsupported {
feature: UnsupportedFeature::SqlDump,
})
}

/// The server's databases the login can open, the connection's own
/// marked as the default and the engine's own as system.
async fn list_databases(&self, _options: CallOptions) -> Result<Vec<SchemaInfo>, DriverError> {
Expand Down
7 changes: 5 additions & 2 deletions core/crates/core/src/ddl/column_attributes.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{
CollationName, ColumnCompression, ColumnKeyRoles, ColumnStorage, ColumnVisibility, GeneratedColumn, IdentityStyle,
UnmodeledAttribute,
CollationName, ColumnCompression, ColumnKeyRoles, ColumnStorage, ColumnVisibility, GeneratedColumn, IdentitySeed,
IdentityStyle, UnmodeledAttribute,
};
use crate::column::SqlExpression;

Expand Down Expand Up @@ -42,6 +42,9 @@ pub struct ColumnAttributes {
/// method rather than following the server's default.
pub compression: Option<ColumnCompression>,
pub identity: IdentityStyle,
/// SQL Server's identity seed and increment, which a CREATE has to
/// state to number the rows as the table does.
pub identity_seed: Option<IdentitySeed>,
/// The SQL Server constraint that holds the default. A type change
/// has to drop it by name first and add it back afterwards.
pub default_constraint_name: Option<String>,
Expand Down
43 changes: 43 additions & 0 deletions core/crates/core/src/ddl/identity_seed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/// SQL Server's `IDENTITY(seed, increment)` as the catalog lists it. Text,
/// since a decimal identity's numbers go past any integer type.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IdentitySeed {
seed: String,
increment: String,
}

impl IdentitySeed {
/// `None` unless both are whole numbers, the only form an identity
/// takes, so nothing else reaches a statement.
pub fn new(seed: &str, increment: &str) -> Option<Self> {
(whole_number(seed) && whole_number(increment)).then(|| Self {
seed: seed.to_owned(),
increment: increment.to_owned(),
})
}

pub fn seed(&self) -> &str {
&self.seed
}

pub fn increment(&self) -> &str {
&self.increment
}
}

fn whole_number(text: &str) -> bool {
let digits = text.strip_prefix('-').unwrap_or(text);
!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn only_whole_numbers_make_a_seed() {
assert!(IdentitySeed::new("-3", "1").is_some());
assert_eq!(IdentitySeed::new("1)", "1"), None);
assert_eq!(IdentitySeed::new("", "1"), None);
}
}
2 changes: 2 additions & 0 deletions core/crates/core/src/ddl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mod draft_table_options;
mod draft_type;
mod drop_table_options;
mod generated_column;
mod identity_seed;
mod identity_style;
mod inbound_foreign_key;
mod order_renames;
Expand Down Expand Up @@ -79,6 +80,7 @@ pub use draft_table_options::DraftTableOptions;
pub use draft_type::{DraftType, ResolvedType};
pub use drop_table_options::DropTableOptions;
pub use generated_column::{GeneratedColumn, GeneratedStorage};
pub use identity_seed::IdentitySeed;
pub use identity_style::IdentityStyle;
pub use inbound_foreign_key::InboundForeignKey;
pub use order_renames::order_renames;
Expand Down
7 changes: 7 additions & 0 deletions core/crates/core/src/dialect/sql_dialect.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::column::{ColumnInfo, ColumnType, SqlExpression};
use crate::ddl::DdlDialect;
use crate::dump::DumpDialect;
use crate::edit::CellExpression;
use crate::filter::{Combinator, FilterOp, operators_for};
use crate::meta::{EngineRowId, EngineRowIdPart, TableRef};
Expand Down Expand Up @@ -89,6 +90,12 @@ pub trait SqlDialect: Send + Sync + std::fmt::Debug {
None
}

/// How the engine writes a SQL dump and runs one back, or `None`
/// where the app has no dump for it.
fn dump(&self) -> Option<&dyn DumpDialect> {
None
}

/// How a page of a table is read. Where the server pages, a page
/// carries no ORDER BY and no window, so a user sort is not offered.
fn table_paging(&self) -> TablePaging {
Expand Down
236 changes: 236 additions & 0 deletions core/crates/core/src/dump/dump_catalog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use crate::meta::TableRef;

/// What a dump covers.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DumpScope {
/// Every schema of the connection's database, the engine's own left
/// out. On an engine whose databases are its schemas, the database the
/// connection is in.
Database,
Schema(String),
}

/// Where an object goes in the file, in the order a restore needs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DumpSection {
/// Schemas, extensions, types, sequences, routines a table can name,
/// and the tables themselves.
BeforeData,
/// What would slow the rows down or refuse them in the wrong order:
/// sequence positions, keys, indexes, foreign keys, comments.
AfterData,
/// Views and routines, ordered by the names their text mentions.
Programs,
/// Last, so loading the rows fires none of them.
Triggers,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DumpObjectKind {
Schema,
Extension,
Collation,
Type,
Sequence,
Function,
Procedure,
Table,
Index,
Constraint,
ForeignKey,
SequenceValue,
Comment,
View,
MaterializedView,
Trigger,
Dictionary,
}

impl DumpObjectKind {
/// The word the file's comment line names the object with.
pub fn label(self) -> &'static str {
match self {
Self::Schema => "Schema",
Self::Extension => "Extension",
Self::Collation => "Collation",
Self::Type => "Type",
Self::Sequence => "Sequence",
Self::Function => "Function",
Self::Procedure => "Procedure",
Self::Table => "Table",
Self::Index => "Index",
Self::Constraint => "Constraint",
Self::ForeignKey => "Foreign key",
Self::SequenceValue => "Sequence value",
Self::Comment => "Comment",
Self::View => "View",
Self::MaterializedView => "Materialized view",
Self::Trigger => "Trigger",
Self::Dictionary => "Dictionary",
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DumpStatement {
/// The statement with no terminator.
pub sql: String,
/// It holds a routine body with semicolons of its own, which MySQL's
/// client reads as one statement only inside a `DELIMITER` block.
pub body: bool,
}

impl DumpStatement {
pub fn new(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
body: false,
}
}

pub fn body(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
body: true,
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DumpObject {
pub kind: DumpObjectKind,
pub section: DumpSection,
pub schema: Option<String>,
pub name: String,
pub statements: Vec<DumpStatement>,
/// The table a CREATE TABLE makes, so it follows the order the rows
/// load in.
pub table: Option<TableRef>,
/// What to write instead in a dump without rows, such as a
/// materialized view made `WITH NO DATA`.
pub without_rows: Option<Vec<DumpStatement>>,
}

impl DumpObject {
pub fn new(
kind: DumpObjectKind,
section: DumpSection,
schema: Option<String>,
name: impl Into<String>,
statements: Vec<DumpStatement>,
) -> Self {
Self {
kind,
section,
schema,
name: name.into(),
statements,
table: None,
without_rows: None,
}
}

/// A CREATE TABLE, placed in the table order.
pub fn table(table: TableRef, statements: Vec<DumpStatement>) -> Self {
Self {
kind: DumpObjectKind::Table,
section: DumpSection::BeforeData,
schema: table.schema.clone(),
name: table.name.clone(),
statements,
table: Some(table),
without_rows: None,
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DumpTable {
/// As the connection reads it.
pub table: TableRef,
/// The tables it must come after: those its foreign keys point at
/// where the engine checks them as rows load, and a partition's
/// parent.
pub references: Vec<TableRef>,
/// Whether its rows are read. A partitioned table's rows are its
/// partitions'.
pub rows: bool,
/// A row can point at a row of its own table and the engine checks
/// that as each statement ends, so each row goes in a statement of
/// its own.
pub one_row_per_insert: bool,
/// Columns the server fills on its own that the file's CREATE makes
/// ordinary, so their values are written: SQL Server's period
/// columns.
pub plain_columns: Vec<String>,
}

impl DumpTable {
pub fn new(table: TableRef) -> Self {
Self {
table,
references: Vec::new(),
rows: true,
one_row_per_insert: false,
plain_columns: Vec::new(),
}
}
}

/// An object the dump leaves out.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DumpSkip {
pub kind: DumpObjectKind,
pub schema: Option<String>,
pub name: String,
pub reason: DumpSkipReason,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DumpSkipReason {
/// The server gave no text for it, as for an object the login may
/// not read.
NoDefinition,
/// Its code is encrypted on the server.
Encrypted,
/// Its code is a .NET assembly, not SQL.
ClrRoutine,
/// Its text holds a line a client reads as the end of a batch, such
/// as a SQL Server line that is only `GO`, so no file can carry it
/// whole.
SplitsApart,
/// Its text hides a secret the server will not show, such as a
/// password.
HiddenSecret,
/// The dump writes no object of its kind, such as a foreign table or
/// an aggregate.
NotSupported,
/// Its definition is in the file but not the rows it stores, such as
/// a materialized view that refills only from rows inserted later.
RowsNotCarried,
/// Its rows live on another server, which a load would write to.
RemoteRows,
/// Row-level security shows the login only some of its rows, so none
/// go in the file.
RowsFiltered,
/// The login may not read it, such as a sequence's position.
NotReadable,
/// A partition whose generated column's expression differs from its
/// parent's, which `PARTITION OF` makes it take: the file's rows get
/// the parent's values.
ExpressionNotCarried,
/// A SQL Server table with a system-time period, written as an
/// ordinary table with its period columns' values. Its history table
/// is an ordinary table too.
VersioningNotCarried,
}

/// What a dump of one scope writes, read from the catalog in one go.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DumpCatalog {
pub objects: Vec<DumpObject>,
pub tables: Vec<DumpTable>,
pub skipped: Vec<DumpSkip>,
}
Loading
Loading