You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by lu...@apache.org on 2026/09/24 21:25:18 UTC
(datafusion-sqlparser-rs) 01/02: SQLite: Support comma-separated WITHOUT ROWID and STRICT table options
This is an automated email from the ASF dual-hosted git repository.
LucaCappelletti94 pushed a commit to branch upstream/sqlite-table-options-list
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit 0eb0b25fcd6bddeaa1451ea658756e7a63a9e696
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Thu Sep 24 22:31:23 2026 +0200
SQLite: Support comma-separated WITHOUT ROWID and STRICT table options
---
src/ast/ddl.rs | 43 +++++++++++++++++++++---------
src/ast/helpers/stmt_create_table.rs | 31 +++++++---------------
src/ast/mod.rs | 4 +--
src/ast/spans.rs | 3 +--
src/parser/mod.rs | 36 ++++++++++++++++++++-----
tests/sqlparser_duckdb.rs | 3 +--
tests/sqlparser_mssql.rs | 6 ++---
tests/sqlparser_postgres.rs | 3 +--
tests/sqlparser_sqlite.rs | 51 +++++++++++++++++++++++++++++++++---
9 files changed, 126 insertions(+), 54 deletions(-)
diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 66f2cad3e..f9c867cc5 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -2908,6 +2908,29 @@ impl fmt::Display for CreateIndex {
}
}
+/// An item of SQLite's `table-options` list, which follows the column
+/// definitions of `CREATE TABLE`.
+///
+/// See [SQLite documentation](https://www.sqlite.org/lang_createtable.html).
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum SqliteTableOption {
+ /// `WITHOUT ROWID`
+ WithoutRowid,
+ /// `STRICT`
+ Strict,
+}
+
+impl fmt::Display for SqliteTableOption {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match self {
+ SqliteTableOption::WithoutRowid => "WITHOUT ROWID",
+ SqliteTableOption::Strict => "STRICT",
+ })
+ }
+}
+
/// CREATE TABLE statement.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -2955,8 +2978,8 @@ pub struct CreateTable {
pub location: Option<String>,
/// Query used to populate the table
pub query: Option<Box<Query>>,
- /// If the table should be created without a rowid (SQLite)
- pub without_rowid: bool,
+ /// SQLite `table-options`, in source order
+ pub sqlite_table_options: Vec<SqliteTableOption>,
/// `LIKE` clause
pub like: Option<CreateTableLikeKind>,
/// `CLONE` clause
@@ -3004,10 +3027,6 @@ pub struct CreateTable {
/// PostgreSQL partition bound specification for PARTITION OF.
/// <https://www.postgresql.org/docs/current/sql-createtable.html>
pub for_values: Option<ForValues>,
- /// SQLite "STRICT" clause.
- /// if the "STRICT" table-option keyword is added to the end, after the closing ")",
- /// then strict typing rules apply to that table.
- pub strict: bool,
/// Snowflake "COPY GRANTS" clause
/// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
pub copy_grants: bool,
@@ -3176,9 +3195,12 @@ impl fmt::Display for CreateTable {
write!(f, " COMMENT '{comment}'")?;
}
- // Only for SQLite
- if self.without_rowid {
- write!(f, " WITHOUT ROWID")?;
+ if !self.sqlite_table_options.is_empty() {
+ write!(
+ f,
+ " {}",
+ display_comma_separated(&self.sqlite_table_options)
+ )?;
}
if let Some(CreateTableLikeKind::Plain(like)) = &self.like {
@@ -3408,9 +3430,6 @@ impl fmt::Display for CreateTable {
};
write!(f, " {on_commit}")?;
}
- if self.strict {
- write!(f, " STRICT")?;
- }
if let Some(backup) = self.backup {
write!(f, " BACKUP {}", if backup { "YES" } else { "NO" })?;
}
diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs
index 84a93dd20..ebd8e006e 100644
--- a/src/ast/helpers/stmt_create_table.rs
+++ b/src/ast/helpers/stmt_create_table.rs
@@ -28,8 +28,8 @@ use crate::ast::{
ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions,
DistStyle, Expr, FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident,
InitializeKind, ObjectName, OnCommit, OneOrManyWithParens, Query, RefreshModeKind,
- RowAccessPolicy, Statement, StorageLifecyclePolicy, StorageSerializationPolicy,
- TableConstraint, TableVersion, Tag, WithData, WrappedCollection,
+ RowAccessPolicy, SqliteTableOption, Statement, StorageLifecyclePolicy,
+ StorageSerializationPolicy, TableConstraint, TableVersion, Tag, WithData, WrappedCollection,
};
use crate::parser::ParserError;
@@ -103,8 +103,8 @@ pub struct CreateTableBuilder {
pub location: Option<String>,
/// Optional `AS SELECT` query for the table.
pub query: Option<Box<Query>>,
- /// Whether `WITHOUT ROWID` is set.
- pub without_rowid: bool,
+ /// SQLite `table-options`, in source order.
+ pub sqlite_table_options: Vec<SqliteTableOption>,
/// Optional `LIKE` clause kind.
pub like: Option<CreateTableLikeKind>,
/// Optional `CLONE` source object name.
@@ -133,8 +133,6 @@ pub struct CreateTableBuilder {
pub partition_of: Option<ObjectName>,
/// Range of values associated with the partition (`FOR VALUES`)
pub for_values: Option<ForValues>,
- /// `STRICT` table flag.
- pub strict: bool,
/// Whether to copy grants from the source.
pub copy_grants: bool,
/// Optional flag for schema evolution support.
@@ -219,7 +217,7 @@ impl CreateTableBuilder {
file_format: None,
location: None,
query: None,
- without_rowid: false,
+ sqlite_table_options: vec![],
like: None,
clone: None,
version: None,
@@ -234,7 +232,6 @@ impl CreateTableBuilder {
inherits: None,
partition_of: None,
for_values: None,
- strict: false,
copy_grants: false,
enable_schema_evolution: None,
change_tracking: None,
@@ -356,9 +353,9 @@ impl CreateTableBuilder {
self.query = query;
self
}
- /// Set `WITHOUT ROWID` option.
- pub fn without_rowid(mut self, without_rowid: bool) -> Self {
- self.without_rowid = without_rowid;
+ /// Set the SQLite `table-options` list.
+ pub fn sqlite_table_options(mut self, sqlite_table_options: Vec<SqliteTableOption>) -> Self {
+ self.sqlite_table_options = sqlite_table_options;
self
}
/// Set `LIKE` clause for the table.
@@ -434,12 +431,6 @@ impl CreateTableBuilder {
self.for_values = for_values;
self
}
-
- /// Set `STRICT` option.
- pub fn strict(mut self, strict: bool) -> Self {
- self.strict = strict;
- self
- }
/// Enable copying grants from source object.
pub fn copy_grants(mut self, copy_grants: bool) -> Self {
self.copy_grants = copy_grants;
@@ -620,7 +611,7 @@ impl CreateTableBuilder {
file_format: self.file_format,
location: self.location,
query: self.query,
- without_rowid: self.without_rowid,
+ sqlite_table_options: self.sqlite_table_options,
like: self.like,
clone: self.clone,
version: self.version,
@@ -635,7 +626,6 @@ impl CreateTableBuilder {
inherits: self.inherits,
partition_of: self.partition_of,
for_values: self.for_values,
- strict: self.strict,
copy_grants: self.copy_grants,
enable_schema_evolution: self.enable_schema_evolution,
change_tracking: self.change_tracking,
@@ -706,7 +696,7 @@ impl From<CreateTable> for CreateTableBuilder {
file_format: table.file_format,
location: table.location,
query: table.query,
- without_rowid: table.without_rowid,
+ sqlite_table_options: table.sqlite_table_options,
like: table.like,
clone: table.clone,
version: table.version,
@@ -721,7 +711,6 @@ impl From<CreateTable> for CreateTableBuilder {
inherits: table.inherits,
partition_of: table.partition_of,
for_values: table.for_values,
- strict: table.strict,
copy_grants: table.copy_grants,
enable_schema_evolution: table.enable_schema_evolution,
change_tracking: table.change_tracking,
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index a025be783..d3ed915dd 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -82,8 +82,8 @@ pub use self::ddl::{
IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes,
OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose,
Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind,
- ReplicaIdentity, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate,
- UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
+ ReplicaIdentity, SqliteTableOption, TagsColumnOption, TextSearchObjectType, TriggerObjectKind,
+ Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
UserDefinedTypeStorage, ViewColumnDef, WithData,
};
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 7acbd7d0b..226228193 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -569,7 +569,7 @@ impl Spanned for CreateTable {
file_format: _, // enum
location: _, // string, no span
query,
- without_rowid: _, // bool
+ sqlite_table_options: _, // keywords, no span
like: _,
clone,
comment: _, // todo, no span
@@ -583,7 +583,6 @@ impl Spanned for CreateTable {
inherits: _, // todo, PostgreSQL specific
partition_of,
for_values,
- strict: _, // bool
copy_grants: _, // bool
enable_schema_evolution: _, // bool
change_tracking: _, // bool
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index a4ee28763..74608ea48 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -8816,8 +8816,7 @@ impl<'a> Parser<'a> {
None
};
- // SQLite supports `WITHOUT ROWID` at the end of `CREATE TABLE`
- let without_rowid = self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]);
+ let sqlite_table_options = self.parse_sqlite_table_options()?;
let hive_distribution = self.parse_hive_distribution()?;
let clustered_by = self.parse_optional_clustered_by()?;
@@ -8868,8 +8867,6 @@ impl<'a> Parser<'a> {
None
};
- let strict = self.parse_keyword(Keyword::STRICT);
-
// Redshift: BACKUP YES|NO
let backup = if self.parse_keyword(Keyword::BACKUP) {
let keyword = self.expect_one_of_keywords(&[Keyword::YES, Keyword::NO])?;
@@ -8935,7 +8932,7 @@ impl<'a> Parser<'a> {
.hive_formats(hive_formats)
.global(global)
.query(query)
- .without_rowid(without_rowid)
+ .sqlite_table_options(sqlite_table_options)
.like(like)
.clone_clause(clone)
.comment_after_column_def(comment_after_column_def)
@@ -8951,7 +8948,6 @@ impl<'a> Parser<'a> {
.table_options(create_table_config.table_options)
.primary_key(primary_key)
.with_data(with_data)
- .strict(strict)
.backup(backup)
.diststyle(diststyle)
.distkey(distkey)
@@ -8959,6 +8955,34 @@ impl<'a> Parser<'a> {
.build())
}
+ /// Parse SQLite's comma-separated `table-options` list, which may be empty.
+ ///
+ /// See <https://www.sqlite.org/lang_createtable.html>.
+ fn parse_sqlite_table_options(&mut self) -> Result<Vec<SqliteTableOption>, ParserError> {
+ let mut options = Vec::new();
+ let Some(first) = self.maybe_parse_sqlite_table_option() else {
+ return Ok(options);
+ };
+ options.push(first);
+ while self.consume_token(&Token::Comma) {
+ match self.maybe_parse_sqlite_table_option() {
+ Some(option) => options.push(option),
+ None => return self.expected_ref("WITHOUT ROWID or STRICT", self.peek_token_ref()),
+ }
+ }
+ Ok(options)
+ }
+
+ fn maybe_parse_sqlite_table_option(&mut self) -> Option<SqliteTableOption> {
+ if self.parse_keywords(&[Keyword::WITHOUT, Keyword::ROWID]) {
+ Some(SqliteTableOption::WithoutRowid)
+ } else if self.parse_keyword(Keyword::STRICT) {
+ Some(SqliteTableOption::Strict)
+ } else {
+ None
+ }
+ }
+
/// Parse `MULTISET` table-kind prefix on `CREATE TABLE`.
fn maybe_parse_multiset(&mut self) -> Option<bool> {
match self.parse_one_of_keywords(&[Keyword::SET, Keyword::MULTISET]) {
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index 51bc77fe6..51ec6f457 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -753,7 +753,7 @@ fn test_duckdb_union_datatype() {
file_format: Default::default(),
location: Default::default(),
query: Default::default(),
- without_rowid: Default::default(),
+ sqlite_table_options: Default::default(),
like: Default::default(),
clone: Default::default(),
comment: Default::default(),
@@ -767,7 +767,6 @@ fn test_duckdb_union_datatype() {
inherits: Default::default(),
partition_of: Default::default(),
for_values: Default::default(),
- strict: Default::default(),
copy_grants: Default::default(),
enable_schema_evolution: Default::default(),
change_tracking: Default::default(),
diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs
index 4510f953e..587e4125f 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -1971,7 +1971,7 @@ fn parse_create_table_with_valid_options() {
file_format: None,
location: None,
query: None,
- without_rowid: false,
+ sqlite_table_options: vec![],
like: None,
clone: None,
comment: None,
@@ -1985,7 +1985,6 @@ fn parse_create_table_with_valid_options() {
inherits: None,
partition_of: None,
for_values: None,
- strict: false,
iceberg: false,
snapshot: false,
copy_grants: false,
@@ -2152,7 +2151,7 @@ fn parse_create_table_with_identity_column() {
file_format: None,
location: None,
query: None,
- without_rowid: false,
+ sqlite_table_options: vec![],
like: None,
clone: None,
comment: None,
@@ -2166,7 +2165,6 @@ fn parse_create_table_with_identity_column() {
inherits: None,
partition_of: None,
for_values: None,
- strict: false,
copy_grants: false,
enable_schema_evolution: None,
change_tracking: None,
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index 23e20c718..7055bac89 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -7049,7 +7049,7 @@ fn parse_trigger_related_functions() {
file_format: None,
location: None,
query: None,
- without_rowid: false,
+ sqlite_table_options: vec![],
like: None,
clone: None,
comment: None,
@@ -7063,7 +7063,6 @@ fn parse_trigger_related_functions() {
inherits: None,
partition_of: None,
for_values: None,
- strict: false,
copy_grants: false,
enable_schema_evolution: None,
change_tracking: None,
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index 22043004a..1bb75f2d7 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -132,9 +132,10 @@ fn parse_create_table_without_rowid() {
match sqlite_and_generic().verified_stmt(sql) {
Statement::CreateTable(CreateTable {
name,
- without_rowid: true,
+ sqlite_table_options,
..
}) => {
+ assert_eq!(sqlite_table_options, vec![SqliteTableOption::WithoutRowid]);
assert_eq!("t", name.to_string());
}
_ => unreachable!(),
@@ -389,9 +390,14 @@ fn test_placeholder() {
#[test]
fn parse_create_table_with_strict() {
let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT";
- if let Statement::CreateTable(CreateTable { name, strict, .. }) = sqlite().verified_stmt(sql) {
+ if let Statement::CreateTable(CreateTable {
+ name,
+ sqlite_table_options,
+ ..
+ }) = sqlite().verified_stmt(sql)
+ {
assert_eq!(name.to_string(), "Fruits");
- assert!(strict);
+ assert_eq!(sqlite_table_options, vec![SqliteTableOption::Strict]);
}
}
@@ -969,6 +975,45 @@ fn parse_n_prefix_not_national_string() {
all_dialects_where(|d| d.supports_national_string_literal()).verified_stmt("SELECT N'hello'");
}
+#[test]
+fn parse_create_table_options_list() {
+ use SqliteTableOption::{Strict, WithoutRowid};
+ for (options, expected) in [
+ ("WITHOUT ROWID, STRICT", vec![WithoutRowid, Strict]),
+ ("STRICT, WITHOUT ROWID", vec![Strict, WithoutRowid]),
+ ("STRICT, STRICT", vec![Strict, Strict]),
+ ] {
+ let sql = format!("CREATE TABLE t (a INT) {options}");
+ match sqlite_and_generic().verified_stmt(&sql) {
+ Statement::CreateTable(CreateTable {
+ sqlite_table_options,
+ ..
+ }) => assert_eq!(sqlite_table_options, expected, "{sql}"),
+ _ => unreachable!(),
+ }
+ }
+
+ for (options, error) in [
+ (
+ "WITHOUT ROWID STRICT",
+ "Expected: end of statement, found: STRICT",
+ ),
+ (
+ "STRICT WITHOUT ROWID",
+ "Expected: end of statement, found: WITHOUT",
+ ),
+ ("STRICT,", "Expected: WITHOUT ROWID or STRICT, found: EOF"),
+ (
+ "STRICT, WITHOUT",
+ "Expected: WITHOUT ROWID or STRICT, found: WITHOUT",
+ ),
+ ] {
+ let sql = format!("CREATE TABLE t (a INT) {options}");
+ let actual = sqlite_and_generic().parse_sql_statements(&sql).unwrap_err();
+ assert!(actual.to_string().contains(error), "{sql}: {actual}");
+ }
+}
+
fn sqlite() -> TestedDialects {
TestedDialects::new(vec![Box::new(SQLiteDialect {})])
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org