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