dirsql 0.3.37__tar.gz → 0.3.39__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. {dirsql-0.3.37 → dirsql-0.3.39}/Cargo.lock +1 -1
  2. {dirsql-0.3.37 → dirsql-0.3.39}/PKG-INFO +1 -1
  3. {dirsql-0.3.37/packages/rust → dirsql-0.3.39}/docs/guide/tables.md +1 -1
  4. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/Cargo.toml +1 -1
  5. {dirsql-0.3.37 → dirsql-0.3.39/packages/python}/docs/guide/tables.md +1 -1
  6. dirsql-0.3.39/packages/python/e2e-attestation.json +6 -0
  7. {dirsql-0.3.37/packages/python → dirsql-0.3.39/packages/rust}/docs/guide/tables.md +1 -1
  8. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/db.rs +164 -15
  9. dirsql-0.3.37/packages/python/e2e-attestation.json +0 -6
  10. {dirsql-0.3.37 → dirsql-0.3.39}/Cargo.toml +0 -0
  11. {dirsql-0.3.37 → dirsql-0.3.39}/README.md +0 -0
  12. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/__init__.py +0 -0
  13. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/_async.py +0 -0
  14. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/_dirsql.pyi +0 -0
  15. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/__init__.py +0 -0
  16. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/binary_path.py +0 -0
  17. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/interpret/__init__.py +0 -0
  18. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/interpret/dispatch_extract.py +0 -0
  19. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/interpret/load_app.py +0 -0
  20. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/interpret/run.py +0 -0
  21. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/interpret/write_message.py +0 -0
  22. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/is_windows.py +0 -0
  23. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/cli/main.py +0 -0
  24. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/py.typed +0 -0
  25. {dirsql-0.3.37 → dirsql-0.3.39}/dirsql/resolve_config.py +0 -0
  26. {dirsql-0.3.37 → dirsql-0.3.39}/docs/.claude/CLAUDE.md +0 -0
  27. {dirsql-0.3.37 → dirsql-0.3.39}/docs/.vitepress/config.ts +0 -0
  28. {dirsql-0.3.37 → dirsql-0.3.39}/docs/.vitepress/theme/index.ts +0 -0
  29. {dirsql-0.3.37 → dirsql-0.3.39}/docs/.vitepress/theme/lang.ts +0 -0
  30. {dirsql-0.3.37 → dirsql-0.3.39}/docs/AGENTS.md +0 -0
  31. {dirsql-0.3.37 → dirsql-0.3.39}/docs/api/index.md +0 -0
  32. {dirsql-0.3.37 → dirsql-0.3.39}/docs/cli/config.md +0 -0
  33. {dirsql-0.3.37 → dirsql-0.3.39}/docs/cli/http-api.md +0 -0
  34. {dirsql-0.3.37 → dirsql-0.3.39}/docs/cli/index.md +0 -0
  35. {dirsql-0.3.37 → dirsql-0.3.39}/docs/cli/init.md +0 -0
  36. {dirsql-0.3.37 → dirsql-0.3.39}/docs/cli/server.md +0 -0
  37. {dirsql-0.3.37 → dirsql-0.3.39}/docs/getting-started.md +0 -0
  38. {dirsql-0.3.37 → dirsql-0.3.39}/docs/guide/async.md +0 -0
  39. {dirsql-0.3.37 → dirsql-0.3.39}/docs/guide/crdt.md +0 -0
  40. {dirsql-0.3.37 → dirsql-0.3.39}/docs/guide/persistence.md +0 -0
  41. {dirsql-0.3.37 → dirsql-0.3.39}/docs/guide/querying.md +0 -0
  42. {dirsql-0.3.37 → dirsql-0.3.39}/docs/guide/watching.md +0 -0
  43. {dirsql-0.3.37 → dirsql-0.3.39}/docs/index.md +0 -0
  44. {dirsql-0.3.37 → dirsql-0.3.39}/docs/migrations.md +0 -0
  45. {dirsql-0.3.37 → dirsql-0.3.39}/docs/package.json +0 -0
  46. {dirsql-0.3.37 → dirsql-0.3.39}/docs/playwright.config.ts +0 -0
  47. {dirsql-0.3.37 → dirsql-0.3.39}/docs/pnpm-lock.yaml +0 -0
  48. {dirsql-0.3.37 → dirsql-0.3.39}/docs/pnpm-workspace.yaml +0 -0
  49. {dirsql-0.3.37 → dirsql-0.3.39}/docs/tests/integration/home.spec.ts +0 -0
  50. {dirsql-0.3.37 → dirsql-0.3.39}/docs/tests/integration/language-flag.spec.ts +0 -0
  51. {dirsql-0.3.37 → dirsql-0.3.39}/docs/tests/unit/config.test.ts +0 -0
  52. {dirsql-0.3.37 → dirsql-0.3.39}/docs/tests/unit/lang.test.ts +0 -0
  53. {dirsql-0.3.37 → dirsql-0.3.39}/docs/vitest.config.ts +0 -0
  54. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/README.md +0 -0
  55. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/conftest.py +0 -0
  56. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/.claude/CLAUDE.md +0 -0
  57. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/.vitepress/config.ts +0 -0
  58. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  59. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  60. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/AGENTS.md +0 -0
  61. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/api/index.md +0 -0
  62. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/cli/config.md +0 -0
  63. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/cli/http-api.md +0 -0
  64. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/cli/index.md +0 -0
  65. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/cli/init.md +0 -0
  66. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/cli/server.md +0 -0
  67. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/getting-started.md +0 -0
  68. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/guide/async.md +0 -0
  69. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/guide/crdt.md +0 -0
  70. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/guide/persistence.md +0 -0
  71. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/guide/querying.md +0 -0
  72. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/guide/watching.md +0 -0
  73. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/index.md +0 -0
  74. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/migrations.md +0 -0
  75. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/package.json +0 -0
  76. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/playwright.config.ts +0 -0
  77. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/pnpm-lock.yaml +0 -0
  78. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/pnpm-workspace.yaml +0 -0
  79. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  80. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  81. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/tests/unit/config.test.ts +0 -0
  82. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  83. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/docs/vitest.config.ts +0 -0
  84. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/src/lib.rs +0 -0
  85. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/__init__.py +0 -0
  86. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/conftest.py +0 -0
  87. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/data/a/meta.json +0 -0
  88. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/data/b/meta.json +0 -0
  89. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/dirsql.config.py +0 -0
  90. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/interpret/data/a/meta.json +0 -0
  91. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/interpret/data/b/meta.json +0 -0
  92. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/interpret/dirsql.config.py +0 -0
  93. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
  94. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__fixtures__/interpret/dirsql.config_raises.py +0 -0
  95. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/__init__.py +0 -0
  96. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/e2e/interpret_subprocess.py +0 -0
  97. {dirsql-0.3.37 → dirsql-0.3.39}/packages/python/tests/integration/__init__.py +0 -0
  98. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/Cargo.toml +0 -0
  99. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/README.md +0 -0
  100. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/benches/db_bench.rs +0 -0
  101. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/benches/differ_bench.rs +0 -0
  102. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/benches/matcher_bench.rs +0 -0
  103. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/benches/scanner_bench.rs +0 -0
  104. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/api/index.md +0 -0
  105. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/cli/config.md +0 -0
  106. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/cli/http-api.md +0 -0
  107. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/cli/index.md +0 -0
  108. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/cli/init.md +0 -0
  109. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/cli/server.md +0 -0
  110. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/getting-started.md +0 -0
  111. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/guide/async.md +0 -0
  112. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/guide/crdt.md +0 -0
  113. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/guide/persistence.md +0 -0
  114. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/guide/querying.md +0 -0
  115. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/guide/watching.md +0 -0
  116. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/index.md +0 -0
  117. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/docs/migrations.md +0 -0
  118. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/bin/dirsql.rs +0 -0
  119. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/cli/init.rs +0 -0
  120. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/cli/mod.rs +0 -0
  121. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/cli/native_config.rs +0 -0
  122. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/cli/router.rs +0 -0
  123. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/cli/serialize.rs +0 -0
  124. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/cli/server.rs +0 -0
  125. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/config.rs +0 -0
  126. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/differ.rs +0 -0
  127. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/lib.rs +0 -0
  128. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/matcher.rs +0 -0
  129. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/persist.rs +0 -0
  130. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/scanner.rs +0 -0
  131. {dirsql-0.3.37 → dirsql-0.3.39}/packages/rust/src/watcher.rs +0 -0
  132. {dirsql-0.3.37 → dirsql-0.3.39}/pyproject.toml +0 -0
@@ -499,7 +499,7 @@ dependencies = [
499
499
 
500
500
  [[package]]
501
501
  name = "dirsql-py-ext"
502
- version = "0.3.37"
502
+ version = "0.3.39"
503
503
  dependencies = [
504
504
  "dirsql",
505
505
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.37
3
+ Version: 0.3.39
4
4
  Requires-Dist: pytest>=8 ; extra == 'dev'
5
5
  Requires-Dist: pytest-describe>=2 ; extra == 'dev'
6
6
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -72,7 +72,7 @@ ddl="CREATE TABLE metrics (name TEXT, value REAL, count INTEGER)"
72
72
  ddl="CREATE TABLE items (id TEXT PRIMARY KEY, name TEXT NOT NULL)"
73
73
  ```
74
74
 
75
- The table name is parsed from the DDL. It must be a valid SQLite identifier.
75
+ The table name is parsed from the DDL and must resolve to a valid SQLite identifier.
76
76
 
77
77
  ### `glob`
78
78
 
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
4
4
  # pypi/maturin handler can rewrite it via `write-version` before
5
5
  # `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
6
6
  # and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
7
- version = "0.3.37"
7
+ version = "0.3.39"
8
8
  edition.workspace = true
9
9
  publish = false
10
10
  readme = "README.md"
@@ -72,7 +72,7 @@ ddl="CREATE TABLE metrics (name TEXT, value REAL, count INTEGER)"
72
72
  ddl="CREATE TABLE items (id TEXT PRIMARY KEY, name TEXT NOT NULL)"
73
73
  ```
74
74
 
75
- The table name is parsed from the DDL. It must be a valid SQLite identifier.
75
+ The table name is parsed from the DDL and must resolve to a valid SQLite identifier.
76
76
 
77
77
  ### `glob`
78
78
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "command": "uv run python -m pytest tests/e2e -q",
3
+ "ran_at": 1782734459,
4
+ "exit_code": 0,
5
+ "commit": "7c5cf2a6e06c5dfabc7ccf60d6b888aeabbaf892"
6
+ }
@@ -72,7 +72,7 @@ ddl="CREATE TABLE metrics (name TEXT, value REAL, count INTEGER)"
72
72
  ddl="CREATE TABLE items (id TEXT PRIMARY KEY, name TEXT NOT NULL)"
73
73
  ```
74
74
 
75
- The table name is parsed from the DDL. It must be a valid SQLite identifier.
75
+ The table name is parsed from the DDL and must resolve to a valid SQLite identifier.
76
76
 
77
77
  ### `glob`
78
78
 
@@ -402,27 +402,89 @@ impl rusqlite::types::ToSql for Value {
402
402
  }
403
403
  }
404
404
 
405
- /// Extract the table name from a CREATE TABLE DDL statement.
406
- /// Handles: CREATE TABLE name (...), CREATE TABLE IF NOT EXISTS name (...)
405
+ /// Extract the table name from a `CREATE TABLE` DDL statement.
406
+ ///
407
+ /// The name token in `CREATE TABLE [IF NOT EXISTS] <name> (...)` may be:
408
+ /// - a bare identifier: `comments`
409
+ /// - a quoted identifier: `"comments"`, `` `comments` ``, `[comments]`
410
+ /// - schema-qualified: `main.comments`, `"main"."comments"`
411
+ ///
412
+ /// Surrounding quotes are SQL *delimiters*, not part of the name, so they are
413
+ /// stripped: a tool that emits `CREATE TABLE "comments" (...)` (as ORMs and
414
+ /// schema generators routinely do) names the table `comments`. Schema-qualified
415
+ /// names resolve to the table segment. Returns `None` when the input isn't a
416
+ /// `CREATE TABLE` or carries no name token.
417
+ ///
418
+ /// This is deliberately a small, pure tokenizer rather than a full SQL parser
419
+ /// (or a round-trip through SQLite): dirsql constrains table names to safe
420
+ /// unquoted identifiers via [`validate_identifier`], so the handful of forms
421
+ /// above are the only ones that can actually resolve to a usable table.
407
422
  pub fn parse_table_name(ddl: &str) -> Option<String> {
408
423
  let upper = ddl.to_uppercase();
409
424
  let idx = upper.find("CREATE TABLE")?;
410
- let rest = &ddl[idx + "CREATE TABLE".len()..].trim_start();
425
+ let mut rest = ddl[idx + "CREATE TABLE".len()..].trim_start();
411
426
 
412
- // Skip optional "IF NOT EXISTS"
413
- let rest = if rest.to_uppercase().starts_with("IF NOT EXISTS") {
414
- rest["IF NOT EXISTS".len()..].trim_start()
415
- } else {
416
- rest
417
- };
427
+ // Skip optional "IF NOT EXISTS". `.get()` avoids slicing on a non-char
428
+ // boundary when the name itself begins with a multi-byte character.
429
+ const IF_NOT_EXISTS: &str = "IF NOT EXISTS";
430
+ if rest
431
+ .get(..IF_NOT_EXISTS.len())
432
+ .is_some_and(|p| p.eq_ignore_ascii_case(IF_NOT_EXISTS))
433
+ {
434
+ rest = rest[IF_NOT_EXISTS.len()..].trim_start();
435
+ }
418
436
 
419
- // Table name is everything up to the first whitespace or '('
420
- let name: String = rest
421
- .chars()
422
- .take_while(|c| !c.is_whitespace() && *c != '(')
423
- .collect();
437
+ // Parse dot-separated identifier segments (`schema.table`) and keep the
438
+ // last one: the table name. Each segment may be quoted independently.
439
+ let mut chars = rest.chars().peekable();
440
+ let mut table = parse_identifier(&mut chars)?;
441
+ while chars.peek() == Some(&'.') {
442
+ chars.next();
443
+ table = parse_identifier(&mut chars)?;
444
+ }
445
+
446
+ if table.is_empty() { None } else { Some(table) }
447
+ }
424
448
 
425
- if name.is_empty() { None } else { Some(name) }
449
+ /// Read one identifier segment from `chars`: a quoted identifier
450
+ /// (`"..."`, `` `...` ``, or `[...]`, with the doubled-delimiter escape for the
451
+ /// matching close on the SQL-standard quote forms) or a bare run up to the
452
+ /// first whitespace, `(`, or `.`. Returns the *unquoted* text. Returns `None`
453
+ /// only when the cursor is already exhausted.
454
+ fn parse_identifier(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<String> {
455
+ let (open, close) = match *chars.peek()? {
456
+ '"' => ('"', '"'),
457
+ '`' => ('`', '`'),
458
+ '[' => ('[', ']'),
459
+ _ => {
460
+ let mut bare = String::new();
461
+ while let Some(&c) = chars.peek() {
462
+ if c.is_whitespace() || c == '(' || c == '.' {
463
+ break;
464
+ }
465
+ bare.push(c);
466
+ chars.next();
467
+ }
468
+ return Some(bare);
469
+ }
470
+ };
471
+
472
+ chars.next(); // consume the opening delimiter
473
+ let mut name = String::new();
474
+ while let Some(c) = chars.next() {
475
+ if c == close {
476
+ // On `"` / `` ` ``, a doubled close delimiter is an escaped literal
477
+ // (`""` -> `"`); SQLite's `[...]` form has no such escape.
478
+ if open != '[' && chars.peek() == Some(&close) {
479
+ chars.next();
480
+ name.push(close);
481
+ continue;
482
+ }
483
+ break;
484
+ }
485
+ name.push(c);
486
+ }
487
+ Some(name)
426
488
  }
427
489
 
428
490
  /// True if `ddl` is a `CREATE VIRTUAL TABLE` statement. dirsql tables are
@@ -630,6 +692,93 @@ mod tests {
630
692
  assert_eq!(parse_table_name("CREATE TABLE "), None);
631
693
  }
632
694
 
695
+ #[test]
696
+ fn parse_table_name_double_quoted() {
697
+ // The canonical ORM / schema-generator shape: the quotes are SQL
698
+ // delimiters and must be stripped to the bare `comments`.
699
+ assert_eq!(
700
+ parse_table_name(r#"CREATE TABLE "comments" (id TEXT)"#),
701
+ Some("comments".to_string())
702
+ );
703
+ }
704
+
705
+ #[test]
706
+ fn parse_table_name_double_quoted_no_space_before_paren() {
707
+ assert_eq!(
708
+ parse_table_name(r#"CREATE TABLE "comments"(id TEXT)"#),
709
+ Some("comments".to_string())
710
+ );
711
+ }
712
+
713
+ #[test]
714
+ fn parse_table_name_backtick_quoted() {
715
+ assert_eq!(
716
+ parse_table_name("CREATE TABLE `comments` (id TEXT)"),
717
+ Some("comments".to_string())
718
+ );
719
+ }
720
+
721
+ #[test]
722
+ fn parse_table_name_bracket_quoted() {
723
+ // SQLite's `[ident]` form ends at the first `]` -- no doubling escape.
724
+ assert_eq!(
725
+ parse_table_name("CREATE TABLE [comments] (id TEXT)"),
726
+ Some("comments".to_string())
727
+ );
728
+ }
729
+
730
+ #[test]
731
+ fn parse_table_name_double_quote_escape() {
732
+ // `""` inside a double-quoted identifier is one literal `"`. The
733
+ // extracted name is later rejected by `validate_identifier`; here we
734
+ // only assert the tokenizer unescapes correctly.
735
+ assert_eq!(
736
+ parse_table_name(r#"CREATE TABLE "we""ird" (id TEXT)"#),
737
+ Some("we\"ird".to_string())
738
+ );
739
+ }
740
+
741
+ #[test]
742
+ fn parse_table_name_if_not_exists_quoted_mixed_case() {
743
+ assert_eq!(
744
+ parse_table_name(r#"CREATE TABLE if not exists "comments" (id TEXT)"#),
745
+ Some("comments".to_string())
746
+ );
747
+ }
748
+
749
+ #[test]
750
+ fn parse_table_name_schema_qualified() {
751
+ // `schema.table` resolves to the table segment (the name SQLite stores
752
+ // in `sqlite_master`).
753
+ assert_eq!(
754
+ parse_table_name("CREATE TABLE main.comments (id TEXT)"),
755
+ Some("comments".to_string())
756
+ );
757
+ }
758
+
759
+ #[test]
760
+ fn parse_table_name_schema_qualified_quoted() {
761
+ assert_eq!(
762
+ parse_table_name(r#"CREATE TABLE "main"."comments" (id TEXT)"#),
763
+ Some("comments".to_string())
764
+ );
765
+ }
766
+
767
+ #[test]
768
+ fn parse_table_name_runs_to_end_of_input() {
769
+ // Bare identifier terminated by EOF rather than whitespace/paren.
770
+ assert_eq!(
771
+ parse_table_name("CREATE TABLE comments"),
772
+ Some("comments".to_string())
773
+ );
774
+ }
775
+
776
+ #[test]
777
+ fn parse_table_name_missing_name_is_none() {
778
+ // A `(` where the name should be yields an empty token -> None.
779
+ assert_eq!(parse_table_name("CREATE TABLE (id TEXT)"), None);
780
+ }
781
+
633
782
  #[test]
634
783
  fn get_table_columns_returns_user_columns_only() {
635
784
  let db = Db::new().unwrap();
@@ -1,6 +0,0 @@
1
- {
2
- "command": "uv run python -m pytest tests/e2e -q",
3
- "ran_at": 1782732207,
4
- "exit_code": 0,
5
- "commit": "6a2dcb8948ae61431b6357df46ce00e8848173ac"
6
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes