You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/10/16 19:38:54 UTC
(datafusion-sqlparser-rs) branch gh-readonly-queue/main/pr-2575-f88f6850717f8665b9228b2d29d3d08831cdceb1 created (now 942f36af)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-2575-f88f6850717f8665b9228b2d29d3d08831cdceb1
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
at 942f36af SQLite: Support comma-separated WITHOUT ROWID and STRICT table options (#2575)
This branch includes the following new commits:
new 942f36af SQLite: Support comma-separated WITHOUT ROWID and STRICT table options (#2575)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org
(datafusion-sqlparser-rs) 01/01: SQLite: Support comma-separated WITHOUT ROWID and STRICT table options (#2575)
Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-2575-f88f6850717f8665b9228b2d29d3d08831cdceb1
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit 942f36afc03163682d00589e46e3012eee1b06ac
Author: Luca Cappelletti <ca...@gmail.com>
AuthorDate: Fri Oct 16 19:38:27 2026 +0000
SQLite: Support comma-separated WITHOUT ROWID and STRICT table options (#2575)
---
src/ast/ddl.rs | 62 ++++++++++++++++++++++++--------
src/ast/helpers/stmt_create_table.rs | 31 ++++++----------
src/ast/mod.rs | 8 ++---
src/ast/spans.rs | 3 +-
src/parser/mod.rs | 47 ++++++++++++++++++++----
tests/sqlparser_duckdb.rs | 3 +-
tests/sqlparser_mssql.rs | 6 ++--
tests/sqlparser_postgres.rs | 3 +-
tests/sqlparser_sqlite.rs | 69 ++++++++++++++++++++++++++++++++++--
9 files changed, 174 insertions(+), 58 deletions(-)
diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 111439cf..5d5b964d 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -2919,6 +2919,41 @@ 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",
+ })
+ }
+}
+
+/// SQLite's `table-options` list of `CREATE TABLE`, empty when absent.
+#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct SqliteTableOptions {
+ /// Whether a comma separates the list from the column definitions,
+ /// as in `(a INT), STRICT`.
+ pub leading_comma: bool,
+ /// The options, in source order.
+ pub options: Vec<SqliteTableOption>,
+}
+
/// CREATE TABLE statement.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -2966,8 +3001,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`
+ pub sqlite_table_options: SqliteTableOptions,
/// `LIKE` clause
pub like: Option<CreateTableLikeKind>,
/// `CLONE` clause
@@ -3015,10 +3050,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,
@@ -3187,11 +3218,6 @@ impl fmt::Display for CreateTable {
write!(f, " COMMENT '{comment}'")?;
}
- // Only for SQLite
- if self.without_rowid {
- write!(f, " WITHOUT ROWID")?;
- }
-
if let Some(CreateTableLikeKind::Plain(like)) = &self.like {
write!(f, " {like}")?;
}
@@ -3204,6 +3230,17 @@ impl fmt::Display for CreateTable {
write!(f, " {version}")?;
}
+ if !self.sqlite_table_options.options.is_empty() {
+ if self.sqlite_table_options.leading_comma {
+ f.write_str(",")?;
+ }
+ write!(
+ f,
+ " {}",
+ display_comma_separated(&self.sqlite_table_options.options)
+ )?;
+ }
+
match &self.hive_distribution {
HiveDistributionStyle::PARTITIONED { columns } => {
write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?;
@@ -3419,9 +3456,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 84a93dd2..4ab9ac2c 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, SqliteTableOptions, 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`.
+ pub sqlite_table_options: SqliteTableOptions,
/// 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: SqliteTableOptions::default(),
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: SqliteTableOptions) -> 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 fcd013aa..c7cd6806 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -82,10 +82,10 @@ 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,
- UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
- UserDefinedTypeStorage, ViewColumnDef, WithData,
+ ReplicaIdentity, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType,
+ TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef,
+ UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation,
+ UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData,
};
pub use self::dml::{
Delete, Insert, InsertOverriding, Merge, MergeAction, MergeClause, MergeClauseKind,
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 5800236f..cf61f1a3 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -570,7 +570,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
@@ -584,7 +584,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 4e2fee94..30e1bc31 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -8912,8 +8912,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()?;
@@ -8954,8 +8953,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])?;
@@ -9021,7 +9018,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)
@@ -9037,7 +9034,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)
@@ -9045,6 +9041,45 @@ impl<'a> Parser<'a> {
.build())
}
+ /// Parse SQLite's `table-options` list, empty when absent, and the
+ /// optional comma before it.
+ ///
+ /// See <https://www.sqlite.org/lang_createtable.html>.
+ fn parse_sqlite_table_options(&mut self) -> Result<SqliteTableOptions, ParserError> {
+ let leading_comma = self.consume_token(&Token::Comma);
+ let Some(first) = self.maybe_parse_sqlite_table_option()? else {
+ if leading_comma {
+ self.prev_token();
+ }
+ return Ok(SqliteTableOptions::default());
+ };
+ let mut options = Vec::with_capacity(2);
+ 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(SqliteTableOptions {
+ leading_comma,
+ options,
+ })
+ }
+
+ fn maybe_parse_sqlite_table_option(
+ &mut self,
+ ) -> Result<Option<SqliteTableOption>, ParserError> {
+ if self.parse_keyword(Keyword::WITHOUT) {
+ self.expect_keyword_is(Keyword::ROWID)?;
+ Ok(Some(SqliteTableOption::WithoutRowid))
+ } else if self.parse_keyword(Keyword::STRICT) {
+ Ok(Some(SqliteTableOption::Strict))
+ } else {
+ Ok(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 158b8637..fe5d29c5 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 369d9770..e07d8700 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -1984,7 +1984,7 @@ fn parse_create_table_with_valid_options() {
file_format: None,
location: None,
query: None,
- without_rowid: false,
+ sqlite_table_options: Default::default(),
like: None,
clone: None,
comment: None,
@@ -1998,7 +1998,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,
@@ -2165,7 +2164,7 @@ fn parse_create_table_with_identity_column() {
file_format: None,
location: None,
query: None,
- without_rowid: false,
+ sqlite_table_options: Default::default(),
like: None,
clone: None,
comment: None,
@@ -2179,7 +2178,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 cb50fbf9..45d3133d 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -7060,7 +7060,7 @@ fn parse_trigger_related_functions() {
file_format: None,
location: None,
query: None,
- without_rowid: false,
+ sqlite_table_options: Default::default(),
like: None,
clone: None,
comment: None,
@@ -7074,7 +7074,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 6223cce9..b19b796c 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -132,9 +132,13 @@ 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.options,
+ vec![SqliteTableOption::WithoutRowid]
+ );
assert_eq!("t", name.to_string());
}
_ => unreachable!(),
@@ -389,9 +393,17 @@ 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.options,
+ vec![SqliteTableOption::Strict]
+ );
}
}
@@ -1054,6 +1066,57 @@ 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 (suffix, leading_comma, options) in [
+ (" WITHOUT ROWID, STRICT", false, vec![WithoutRowid, Strict]),
+ (" STRICT, WITHOUT ROWID", false, vec![Strict, WithoutRowid]),
+ (" STRICT, STRICT", false, vec![Strict, Strict]),
+ (", STRICT", true, vec![Strict]),
+ (", WITHOUT ROWID, STRICT", true, vec![WithoutRowid, Strict]),
+ ] {
+ let sql = format!("CREATE TABLE t (a INT){suffix}");
+ match sqlite_and_generic().verified_stmt(&sql) {
+ Statement::CreateTable(CreateTable {
+ sqlite_table_options,
+ ..
+ }) => assert_eq!(
+ sqlite_table_options,
+ SqliteTableOptions {
+ leading_comma,
+ options
+ },
+ "{sql}"
+ ),
+ _ => unreachable!(),
+ }
+ }
+ sqlite_and_generic().one_statement_parses_to(
+ "CREATE TABLE t (a INT) , STRICT",
+ "CREATE TABLE t (a INT), STRICT",
+ );
+
+ for (suffix, 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: ROWID, found: EOF"),
+ (",", "Expected: end of statement, found: ,"),
+ (", , STRICT", "Expected: end of statement, found: ,"),
+ ] {
+ let sql = format!("CREATE TABLE t (a INT){suffix}");
+ 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