Preserve the opening token of `CREATE TABLE`
diff --git a/fuzz/fuzz_seeds/generic_1548_create_table_span.sql b/fuzz/fuzz_seeds/generic_1548_create_table_span.sql
new file mode 100644
index 0000000..d8a588b
--- /dev/null
+++ b/fuzz/fuzz_seeds/generic_1548_create_table_span.sql
@@ -0,0 +1 @@
+CREATE TABLE t (id INT)
diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 5d5b964..1035123 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -2959,6 +2959,8 @@
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
 pub struct CreateTable {
+    /// The opening `CREATE` token, empty for tables constructed without one.
+    pub create_token: AttachedToken,
     /// `OR REPLACE` clause
     pub or_replace: bool,
     /// `TEMP` or `TEMPORARY` clause
diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs
index 4ab9ac2..2cc13b5 100644
--- a/src/ast/helpers/stmt_create_table.rs
+++ b/src/ast/helpers/stmt_create_table.rs
@@ -24,6 +24,7 @@
 #[cfg(feature = "visitor")]
 use sqlparser_derive::{Visit, VisitMut};
 
+use crate::ast::helpers::attached_token::AttachedToken;
 use crate::ast::{
     ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions,
     DistStyle, Expr, FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident,
@@ -65,6 +66,8 @@
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
 pub struct CreateTableBuilder {
+    /// The opening `CREATE` token, empty for tables constructed without one.
+    pub create_token: AttachedToken,
     /// Whether the statement uses `OR REPLACE`.
     pub or_replace: bool,
     /// Whether the table is `TEMPORARY`.
@@ -198,6 +201,7 @@
     /// Create a new `CreateTableBuilder` for the given table name.
     pub fn new(name: ObjectName) -> Self {
         Self {
+            create_token: AttachedToken::empty(),
             or_replace: false,
             temporary: false,
             unlogged: false,
@@ -263,6 +267,11 @@
             with_data: None,
         }
     }
+    /// Set the opening `CREATE` token.
+    pub fn create_token(mut self, create_token: AttachedToken) -> Self {
+        self.create_token = create_token;
+        self
+    }
     /// Set `OR REPLACE` for the CREATE TABLE statement.
     pub fn or_replace(mut self, or_replace: bool) -> Self {
         self.or_replace = or_replace;
@@ -592,6 +601,7 @@
     /// Consume the builder and produce a `CreateTable`.
     pub fn build(self) -> CreateTable {
         CreateTable {
+            create_token: self.create_token,
             or_replace: self.or_replace,
             temporary: self.temporary,
             unlogged: self.unlogged,
@@ -677,6 +687,7 @@
 impl From<CreateTable> for CreateTableBuilder {
     fn from(table: CreateTable) -> Self {
         Self {
+            create_token: table.create_token,
             or_replace: table.or_replace,
             temporary: table.temporary,
             unlogged: table.unlogged,
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index eab9f71..894917d 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -551,6 +551,7 @@
 impl Spanned for CreateTable {
     fn span(&self) -> Span {
         let CreateTable {
+            create_token,
             or_replace: _,    // bool
             temporary: _,     // bool
             unlogged: _,      // bool
@@ -617,7 +618,8 @@
         } = self;
 
         union_spans(
-            core::iter::once(name.span())
+            core::iter::once(create_token.0.span)
+                .chain(core::iter::once(name.span()))
                 .chain(core::iter::once(table_options.span()))
                 .chain(columns.iter().map(|i| i.span()))
                 .chain(constraints.iter().map(|i| i.span()))
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index dc462f5..bed3c9b 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -313,6 +313,7 @@
         }
 
         if parser.parse_keyword(Keyword::CREATE) {
+            let create_token_index = parser.get_current_index();
             // possibly CREATE STAGE
             //[ OR  REPLACE ]
             let or_replace = parser.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);
@@ -353,7 +354,10 @@
                         or_replace, global, temporary, volatile, transient, iceberg, dynamic,
                         parser,
                     )
-                    .map(Into::into),
+                    .map(|mut table| {
+                        table.create_token = parser.token_at(create_token_index).clone().into();
+                        table.into()
+                    }),
                 );
             } else if parser.parse_keyword(Keyword::DATABASE) {
                 return Some(parse_create_database(or_replace, transient, parser));
@@ -2031,3 +2035,22 @@
 
     Ok((when_clauses, else_clause))
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::ast::Spanned;
+    use crate::tokenizer::{Span, Token};
+
+    #[test]
+    fn create_table_span_snowflake_direct_entry() {
+        let mut parser = Parser::new(&SnowflakeDialect)
+            .try_with_sql("t (id INT); SELECT 1")
+            .unwrap();
+        let table = parse_create_table(false, None, false, false, false, false, false, &mut parser)
+            .unwrap();
+        assert_eq!(table.create_token.0.span, Span::empty());
+        assert_eq!(table.span().start, table.name.span().start);
+        assert_eq!(parser.peek_token_ref().token, Token::SemiColon);
+    }
+}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 95ff188..8805119 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -5352,6 +5352,9 @@
 
     /// Parse a SQL CREATE statement
     pub fn parse_create(&mut self) -> Result<Statement, ParserError> {
+        let create_token_index = self.index.checked_sub(1).filter(|&index| {
+            matches!(&self.token_at(index).token, Token::Word(word) if word.keyword == Keyword::CREATE)
+        });
         let modifier_loc = self.peek_token_ref().span.start;
         let or_replace = self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]);
         let or_alter = self.parse_keywords(&[Keyword::OR, Keyword::ALTER]);
@@ -5377,7 +5380,7 @@
         let persistent = dialect_of!(self is DuckDbDialect)
             && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some();
         let create_view_params = self.parse_create_view_params()?;
-        if self.peek_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) {
+        let mut statement = if self.peek_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) {
             self.parse_create_snapshot_table().map(Into::into)
         } else if self.peek_keywords(&[Keyword::TEXT, Keyword::SEARCH]) {
             self.parse_create_text_search().map(Into::into)
@@ -5478,7 +5481,13 @@
             self.parse_create_foreign_table().map(Into::into)
         } else {
             self.expected_ref("an object type after CREATE", self.peek_token_ref())
+        }?;
+        if let Statement::CreateTable(table) = &mut statement {
+            if let Some(index) = create_token_index {
+                table.create_token = self.token_at(index).clone().into();
+            }
         }
+        Ok(statement)
     }
 
     fn parse_text_search_object_type(&mut self) -> Result<TextSearchObjectType, ParserError> {
diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs
index daebe63..66c5b5f 100644
--- a/tests/sqlparser_bigquery.rs
+++ b/tests/sqlparser_bigquery.rs
@@ -3001,3 +3001,27 @@
         err
     );
 }
+
+#[test]
+fn create_table_span_snapshot() {
+    use sqlparser::ast::Spanned;
+    use sqlparser::parser::Parser;
+    use sqlparser::tokenizer::Location;
+
+    for canonical in [
+        "CREATE SNAPSHOT TABLE t CLONE source",
+        "CREATE SNAPSHOT TABLE IF NOT EXISTS t CLONE source",
+        "CREATE OR REPLACE EXTERNAL TABLE t (id INT)",
+    ] {
+        bigquery_and_generic().verified_stmt(canonical);
+        bigquery_and_generic().one_of_identical_results(|dialect| {
+            let sql = format!("SELECT 1;\n  {canonical}");
+            let statements = Parser::parse_sql(dialect, &sql).unwrap();
+            let Statement::CreateTable(table) = &statements[1] else {
+                panic!("Expected CREATE TABLE");
+            };
+            assert_eq!(table.span().start, Location::new(2, 3));
+            assert_eq!(statements[1].span().start, Location::new(2, 3));
+        });
+    }
+}
diff --git a/tests/sqlparser_comments.rs b/tests/sqlparser_comments.rs
index ed78a77..e985346 100644
--- a/tests/sqlparser_comments.rs
+++ b/tests/sqlparser_comments.rs
@@ -180,3 +180,24 @@
         }]
     );
 }
+
+#[test]
+fn create_table_span_find_before() {
+    use sqlparser::ast::{Spanned, Statement};
+
+    let sql = r#"-- users docs
+CREATE TABLE IF NOT EXISTS users (id INT)"#;
+    all_dialects().one_of_identical_results(|dialect| {
+        let (statements, comments) = Parser::parse_sql_with_comments(dialect, sql).unwrap();
+        let Statement::CreateTable(table) = &statements[0] else {
+            panic!("Expected CREATE TABLE");
+        };
+        assert_eq!(
+            comments
+                .find_before(table.span().start)
+                .map(|comment| comment.as_str())
+                .collect::<Vec<_>>(),
+            vec![" users docs"]
+        );
+    });
+}
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 5a5c2b3..41eaf59 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -20856,3 +20856,134 @@
         "SELECT count(*) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t",
     );
 }
+
+#[test]
+fn create_table_span_starts_at_create() {
+    for (sql, expected_start) in [
+        ("CREATE TABLE t (id INT)", Location::new(1, 1)),
+        (
+            "CREATE TABLE IF NOT EXISTS users (id INT)",
+            Location::new(1, 1),
+        ),
+        (
+            r#"-- table docs
+CREATE TABLE
+  users (id INT)"#,
+            Location::new(2, 1),
+        ),
+        ("  CREATE TEMPORARY TABLE t (id INT)", Location::new(1, 3)),
+    ] {
+        all_dialects().one_of_identical_results(|dialect| {
+            let statements = Parser::parse_sql(dialect, sql).unwrap();
+            let Statement::CreateTable(table) = &statements[0] else {
+                panic!("Expected CREATE TABLE");
+            };
+            assert_eq!(statements[0].span().start, expected_start, "{sql}");
+            assert_eq!(table.span().start, expected_start, "{sql}");
+        });
+    }
+}
+
+#[test]
+fn create_table_span_prefixes() {
+    for prefix in [
+        "CREATE",
+        "CREATE OR REPLACE",
+        "CREATE TEMPORARY",
+        "CREATE OR REPLACE TEMPORARY",
+        "CREATE LOCAL TEMPORARY",
+        "CREATE GLOBAL TEMPORARY",
+        "CREATE TRANSIENT",
+        "CREATE VOLATILE",
+        "CREATE UNLOGGED",
+        "CREATE SET",
+        "CREATE MULTISET",
+        "CREATE MULTISET VOLATILE",
+        "CREATE EXTERNAL",
+    ] {
+        let canonical = format!("{prefix} TABLE IF NOT EXISTS t (id INT)");
+        all_dialects().verified_stmt(&canonical);
+        all_dialects().one_of_identical_results(|dialect| {
+            let sql = format!("SELECT 1;\n  {canonical}");
+            let statements = Parser::parse_sql(dialect, &sql).unwrap();
+            assert_eq!(statements[1].span().start, Location::new(2, 3), "{sql}");
+        });
+    }
+}
+
+#[test]
+fn create_table_span_semantic_identity() {
+    use std::collections::hash_map::DefaultHasher;
+    use std::hash::{Hash, Hasher};
+
+    fn hash(value: &impl Hash) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        value.hash(&mut hasher);
+        hasher.finish()
+    }
+
+    all_dialects().one_of_identical_results(|dialect| {
+        let sql = "CREATE TABLE t AS SELECT 1";
+        let original = Parser::parse_sql(dialect, sql).unwrap().remove(0);
+        let shifted = Parser::parse_sql(dialect, &format!("\n  {sql}"))
+            .unwrap()
+            .remove(0);
+        assert_eq!(original, shifted);
+        assert_eq!(original.cmp(&shifted), core::cmp::Ordering::Equal);
+        assert_eq!(hash(&original), hash(&shifted));
+        let (Statement::CreateTable(original), Statement::CreateTable(shifted)) =
+            (&original, &shifted)
+        else {
+            panic!("Expected CREATE TABLE");
+        };
+        assert_ne!(original.span().start, shifted.span().start);
+        assert_eq!(original, shifted);
+        assert_eq!(original.cmp(shifted), core::cmp::Ordering::Equal);
+        assert_eq!(hash(original), hash(shifted));
+        assert_eq!(
+            original.span().end,
+            original.query.as_ref().unwrap().span().end
+        );
+        assert_eq!(
+            shifted.span().end,
+            shifted.query.as_ref().unwrap().span().end
+        );
+    });
+}
+
+#[test]
+fn create_table_span_direct_entries() {
+    for (sql, consume_create, expected_start) in [
+        (
+            "  CREATE TABLE t (id INT); SELECT 1",
+            true,
+            Location::new(1, 3),
+        ),
+        ("TABLE t (id INT); SELECT 1", false, Location::new(1, 7)),
+    ] {
+        let mut parser = Parser::new(&GenericDialect).try_with_sql(sql).unwrap();
+        if consume_create {
+            parser.expect_keyword_is(Keyword::CREATE).unwrap();
+        }
+        let statement = parser.parse_create().unwrap();
+        assert_eq!(statement.span().start, expected_start);
+        assert_eq!(parser.peek_token_ref().token, Token::SemiColon);
+    }
+
+    for (entry, sql) in [
+        (0, "t (id INT); SELECT 1"),
+        (1, "TABLE t (id INT); SELECT 1"),
+        (2, "SNAPSHOT TABLE t CLONE source; SELECT 1"),
+    ] {
+        let mut parser = Parser::new(&GenericDialect).try_with_sql(sql).unwrap();
+        let table = match entry {
+            0 => parser.parse_create_table(false, false, false, None, false, false, None),
+            1 => parser.parse_create_external_table(false),
+            _ => parser.parse_create_snapshot_table(),
+        }
+        .unwrap();
+        assert_eq!(table.create_token.0.span, Span::empty());
+        assert_eq!(table.span().start, table.name.span().start);
+        assert_eq!(parser.peek_token_ref().token, Token::SemiColon);
+    }
+}
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index 216d514..bfeae95 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -700,6 +700,7 @@
     let stmt = duckdb_and_generic().verified_stmt(sql);
     assert_eq!(
         Statement::CreateTable(CreateTable {
+            create_token: AttachedToken::empty(),
             or_replace: Default::default(),
             temporary: Default::default(),
             unlogged: Default::default(),
diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs
index e07d870..37d98d2 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -1935,6 +1935,7 @@
         assert_eq!(
             ms_and_generic().verified_stmt(sql),
             Statement::CreateTable(CreateTable {
+                create_token: AttachedToken::empty(),
                 or_replace: false,
                 temporary: false,
                 unlogged: false,
@@ -2132,6 +2133,7 @@
         assert_eq!(
             ms_and_generic().verified_stmt(sql),
             Statement::CreateTable(CreateTable {
+                create_token: AttachedToken::empty(),
                 or_replace: false,
                 temporary: false,
                 unlogged: false,
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index 1e00288..5556f23 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -7020,6 +7020,7 @@
     assert_eq!(
         create_table,
         CreateTable {
+            create_token: AttachedToken::empty(),
             or_replace: false,
             temporary: false,
             unlogged: false,
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index 6c8c73c..2726f9e 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -23,7 +23,7 @@
 use sqlparser::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageLoadSelectItemKind};
 use sqlparser::ast::*;
 use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect};
-use sqlparser::parser::{ParserError, ParserOptions};
+use sqlparser::parser::{Parser, ParserError, ParserOptions};
 use sqlparser::tokenizer::*;
 use test_utils::*;
 
@@ -5101,3 +5101,26 @@
         ParserError::ParserError("Expected: stage name identifier, found: .".to_string()),
     );
 }
+
+#[test]
+fn create_table_span_snowflake_prefixes() {
+    use sqlparser::ast::Spanned;
+    use sqlparser::tokenizer::Location;
+
+    for canonical in [
+        "CREATE OR REPLACE TRANSIENT TABLE t (id INT)",
+        "CREATE ICEBERG TABLE t (id INT) BASE_LOCATION='t'",
+        "CREATE DYNAMIC TABLE t TARGET_LAG='DOWNSTREAM' WAREHOUSE=wh AS SELECT 1",
+        "CREATE DYNAMIC ICEBERG TABLE t BASE_LOCATION='t' TARGET_LAG='DOWNSTREAM' WAREHOUSE=wh AS SELECT 1",
+        "CREATE GLOBAL TEMPORARY TABLE IF NOT EXISTS t (id INT)",
+    ] {
+        snowflake().verified_stmt(canonical);
+        let sql = format!("SELECT 1;\n  {canonical}");
+        let statements = Parser::parse_sql(&SnowflakeDialect, &sql).unwrap();
+        let Statement::CreateTable(table) = &statements[1] else {
+            panic!("Expected CREATE TABLE");
+        };
+        assert_eq!(table.span().start, Location::new(2, 3));
+        assert_eq!(statements[1].span().start, Location::new(2, 3));
+    }
+}