rbtr-lang-sql 2026.9.0.dev1__py3-none-any.whl

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 (26) hide show
  1. rbtr_lang_sql/__init__.py +1 -0
  2. rbtr_lang_sql/plugin.py +52 -0
  3. rbtr_lang_sql/py.typed +0 -0
  4. rbtr_lang_sql/sql.scm +70 -0
  5. rbtr_lang_sql/tests/__init__.py +0 -0
  6. rbtr_lang_sql/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +1 -0
  7. rbtr_lang_sql/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +268 -0
  8. rbtr_lang_sql/tests/__snapshots__/test_samples/test_sql_dialect_extraction_matches_snapshot[sql_clickhouse].json +59 -0
  9. rbtr_lang_sql/tests/__snapshots__/test_samples/test_sql_dialect_extraction_matches_snapshot[sql_duckdb].json +78 -0
  10. rbtr_lang_sql/tests/__snapshots__/test_samples/test_sql_dialect_extraction_matches_snapshot[sql_mysql].json +59 -0
  11. rbtr_lang_sql/tests/__snapshots__/test_samples/test_sql_dialect_extraction_matches_snapshot[sql_postgres].json +116 -0
  12. rbtr_lang_sql/tests/__snapshots__/test_samples/test_sql_dialect_extraction_matches_snapshot[sql_sqlite].json +78 -0
  13. rbtr_lang_sql/tests/cases_extraction.py +265 -0
  14. rbtr_lang_sql/tests/samples/sql/sql.sql +38 -0
  15. rbtr_lang_sql/tests/samples/sql_clickhouse.sql +17 -0
  16. rbtr_lang_sql/tests/samples/sql_duckdb.sql +14 -0
  17. rbtr_lang_sql/tests/samples/sql_mysql.sql +11 -0
  18. rbtr_lang_sql/tests/samples/sql_postgres.sql +18 -0
  19. rbtr_lang_sql/tests/samples/sql_sqlite.sql +11 -0
  20. rbtr_lang_sql/tests/test_extraction.py +85 -0
  21. rbtr_lang_sql/tests/test_samples.py +115 -0
  22. rbtr_lang_sql-2026.9.0.dev1.dist-info/METADATA +79 -0
  23. rbtr_lang_sql-2026.9.0.dev1.dist-info/RECORD +26 -0
  24. rbtr_lang_sql-2026.9.0.dev1.dist-info/WHEEL +4 -0
  25. rbtr_lang_sql-2026.9.0.dev1.dist-info/entry_points.txt +3 -0
  26. rbtr_lang_sql-2026.9.0.dev1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,116 @@
1
+ [
2
+ {
3
+ "blob_sha": "sha1",
4
+ "file_path": "sql_postgres.sql",
5
+ "kind": "comment",
6
+ "name": "",
7
+ "scope": "",
8
+ "language": "sql",
9
+ "file_language": "sql",
10
+ "content": "-- PostgreSQL dialect sample — SERIAL, JSONB, arrays, ENUM type, plpgsql.",
11
+ "line_start": 1,
12
+ "line_end": 1,
13
+ "metadata": {
14
+ "module": "",
15
+ "names": "",
16
+ "dots": "",
17
+ "language_hint": ""
18
+ },
19
+ "id": "0202263c4558210b"
20
+ },
21
+ {
22
+ "blob_sha": "sha1",
23
+ "file_path": "sql_postgres.sql",
24
+ "kind": "variable",
25
+ "name": "\"uuid-ossp\"",
26
+ "scope": "",
27
+ "language": "sql",
28
+ "file_language": "sql",
29
+ "content": "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"",
30
+ "line_start": 3,
31
+ "line_end": 3,
32
+ "metadata": {
33
+ "module": "",
34
+ "names": "",
35
+ "dots": "",
36
+ "language_hint": ""
37
+ },
38
+ "id": "ea569708fd9107dd"
39
+ },
40
+ {
41
+ "blob_sha": "sha1",
42
+ "file_path": "sql_postgres.sql",
43
+ "kind": "class",
44
+ "name": "mood",
45
+ "scope": "",
46
+ "language": "sql",
47
+ "file_language": "sql",
48
+ "content": "CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')",
49
+ "line_start": 5,
50
+ "line_end": 5,
51
+ "metadata": {
52
+ "module": "",
53
+ "names": "",
54
+ "dots": "",
55
+ "language_hint": ""
56
+ },
57
+ "id": "8935d150afcbbe2f"
58
+ },
59
+ {
60
+ "blob_sha": "sha1",
61
+ "file_path": "sql_postgres.sql",
62
+ "kind": "class",
63
+ "name": "users",
64
+ "scope": "",
65
+ "language": "sql",
66
+ "file_language": "sql",
67
+ "content": "CREATE TABLE users (\n id SERIAL PRIMARY KEY,\n email TEXT UNIQUE NOT NULL,\n profile JSONB DEFAULT '{}'::jsonb,\n tags TEXT[],\n created_at TIMESTAMPTZ DEFAULT now()\n)",
68
+ "line_start": 7,
69
+ "line_end": 13,
70
+ "metadata": {
71
+ "module": "",
72
+ "names": "",
73
+ "dots": "",
74
+ "language_hint": ""
75
+ },
76
+ "id": "c959eda6cfcc840d"
77
+ },
78
+ {
79
+ "blob_sha": "sha1",
80
+ "file_path": "sql_postgres.sql",
81
+ "kind": "function",
82
+ "name": "greet",
83
+ "scope": "",
84
+ "language": "sql",
85
+ "file_language": "sql",
86
+ "content": "CREATE FUNCTION greet(name TEXT) RETURNS TEXT\nLANGUAGE sql AS $$ SELECT 'Hello, ' || name $$",
87
+ "line_start": 15,
88
+ "line_end": 16,
89
+ "metadata": {
90
+ "module": "",
91
+ "names": "",
92
+ "dots": "",
93
+ "language_hint": ""
94
+ },
95
+ "id": "494263cbd1532edd"
96
+ },
97
+ {
98
+ "blob_sha": "sha1",
99
+ "file_path": "sql_postgres.sql",
100
+ "kind": "variable",
101
+ "name": "idx_users_email",
102
+ "scope": "",
103
+ "language": "sql",
104
+ "file_language": "sql",
105
+ "content": "CREATE INDEX idx_users_email ON users (email)",
106
+ "line_start": 18,
107
+ "line_end": 18,
108
+ "metadata": {
109
+ "module": "",
110
+ "names": "",
111
+ "dots": "",
112
+ "language_hint": ""
113
+ },
114
+ "id": "64c23d29e5209b80"
115
+ }
116
+ ]
@@ -0,0 +1,78 @@
1
+ [
2
+ {
3
+ "blob_sha": "sha1",
4
+ "file_path": "sql_sqlite.sql",
5
+ "kind": "comment",
6
+ "name": "",
7
+ "scope": "",
8
+ "language": "sql",
9
+ "file_language": "sql",
10
+ "content": "-- SQLite dialect sample — AUTOINCREMENT, WITHOUT ROWID, IF NOT EXISTS.",
11
+ "line_start": 1,
12
+ "line_end": 1,
13
+ "metadata": {
14
+ "module": "",
15
+ "names": "",
16
+ "dots": "",
17
+ "language_hint": ""
18
+ },
19
+ "id": "0202263c4558210b"
20
+ },
21
+ {
22
+ "blob_sha": "sha1",
23
+ "file_path": "sql_sqlite.sql",
24
+ "kind": "class",
25
+ "name": "users",
26
+ "scope": "",
27
+ "language": "sql",
28
+ "file_language": "sql",
29
+ "content": "CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n email TEXT NOT NULL UNIQUE,\n created_at TEXT DEFAULT (datetime('now'))\n)",
30
+ "line_start": 3,
31
+ "line_end": 7,
32
+ "metadata": {
33
+ "module": "",
34
+ "names": "",
35
+ "dots": "",
36
+ "language_hint": ""
37
+ },
38
+ "id": "bbc0c1d3c758c83c"
39
+ },
40
+ {
41
+ "blob_sha": "sha1",
42
+ "file_path": "sql_sqlite.sql",
43
+ "kind": "variable",
44
+ "name": "idx_users_email",
45
+ "scope": "",
46
+ "language": "sql",
47
+ "file_language": "sql",
48
+ "content": "CREATE INDEX idx_users_email ON users (email)",
49
+ "line_start": 9,
50
+ "line_end": 9,
51
+ "metadata": {
52
+ "module": "",
53
+ "names": "",
54
+ "dots": "",
55
+ "language_hint": ""
56
+ },
57
+ "id": "ccab971541354b72"
58
+ },
59
+ {
60
+ "blob_sha": "sha1",
61
+ "file_path": "sql_sqlite.sql",
62
+ "kind": "class",
63
+ "name": "active_users",
64
+ "scope": "",
65
+ "language": "sql",
66
+ "file_language": "sql",
67
+ "content": "CREATE VIEW active_users AS SELECT id, email FROM users",
68
+ "line_start": 11,
69
+ "line_end": 11,
70
+ "metadata": {
71
+ "module": "",
72
+ "names": "",
73
+ "dots": "",
74
+ "language_hint": ""
75
+ },
76
+ "id": "99cce937484e7657"
77
+ }
78
+ ]
@@ -0,0 +1,265 @@
1
+ """SQL extraction test cases.
2
+
3
+ Each `@case` returns test data consumed by `test_extraction.py` via
4
+ `pytest-cases`. The cases are the spec of the SQL constructs the plugin
5
+ captures — one chunk per top-level statement (plus one per CTE), named
6
+ by its object/table (SQL has no nesting, so scope is always ""). See the
7
+ plugin docstring for the full source→chunk mapping.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pytest_cases import case
13
+
14
+ type SymbolCase = tuple[str, str, list[tuple[str, str, str]]]
15
+ type MixedCase = tuple[str, str, set[str], list[tuple[str, str]]]
16
+
17
+
18
+ # ── DDL definitions: class ───────────────────────────────────────────
19
+
20
+
21
+ @case(tags=["symbol"])
22
+ def case_sql_table() -> SymbolCase:
23
+ """CREATE TABLE — structural definition → class."""
24
+ return "sql", "CREATE TABLE users (id INT, name TEXT);\n", [("class", "users", "")]
25
+
26
+
27
+ @case(tags=["symbol"])
28
+ def case_sql_view() -> SymbolCase:
29
+ """CREATE VIEW → class."""
30
+ return "sql", "CREATE VIEW active AS SELECT * FROM users;\n", [("class", "active", "")]
31
+
32
+
33
+ @case(tags=["symbol"])
34
+ def case_sql_materialized_view() -> SymbolCase:
35
+ """CREATE MATERIALIZED VIEW → class."""
36
+ src = "CREATE MATERIALIZED VIEW recent AS SELECT * FROM users;\n"
37
+ return "sql", src, [("class", "recent", "")]
38
+
39
+
40
+ @case(tags=["symbol"])
41
+ def case_sql_type_enum() -> SymbolCase:
42
+ """CREATE TYPE ... AS ENUM — SQL's enum → class."""
43
+ return "sql", "CREATE TYPE mood AS ENUM ('sad', 'happy');\n", [("class", "mood", "")]
44
+
45
+
46
+ @case(tags=["symbol"])
47
+ def case_sql_type_composite() -> SymbolCase:
48
+ """CREATE TYPE ... AS (...) — composite type → class."""
49
+ src = "CREATE TYPE point AS (x DOUBLE PRECISION, y DOUBLE PRECISION);\n"
50
+ return "sql", src, [("class", "point", "")]
51
+
52
+
53
+ # ── DDL definitions: variable (standalone named objects) ─────────────
54
+
55
+
56
+ @case(tags=["symbol"])
57
+ def case_sql_index() -> SymbolCase:
58
+ """CREATE INDEX → variable, named by the index (not the ON table)."""
59
+ return "sql", "CREATE INDEX idx_name ON users (name);\n", [("variable", "idx_name", "")]
60
+
61
+
62
+ @case(tags=["symbol"])
63
+ def case_sql_sequence() -> SymbolCase:
64
+ """CREATE SEQUENCE → variable."""
65
+ return "sql", "CREATE SEQUENCE order_id START 1;\n", [("variable", "order_id", "")]
66
+
67
+
68
+ @case(tags=["symbol"])
69
+ def case_sql_schema() -> SymbolCase:
70
+ """CREATE SCHEMA → variable."""
71
+ return "sql", "CREATE SCHEMA app;\n", [("variable", "app", "")]
72
+
73
+
74
+ @case(tags=["symbol"])
75
+ def case_sql_extension() -> SymbolCase:
76
+ """CREATE EXTENSION → variable (common in migrations)."""
77
+ return "sql", "CREATE EXTENSION postgis;\n", [("variable", "postgis", "")]
78
+
79
+
80
+ @case(tags=["symbol"])
81
+ def case_sql_trigger() -> SymbolCase:
82
+ """CREATE TRIGGER → variable, named by the trigger (not its table/function)."""
83
+ src = """\
84
+ CREATE TRIGGER audit BEFORE UPDATE ON users
85
+ FOR EACH ROW EXECUTE FUNCTION log_change();
86
+ """
87
+ return "sql", src, [("variable", "audit", "")]
88
+
89
+
90
+ # ── Routines and statements: function ────────────────────────────────
91
+
92
+
93
+ @case(tags=["symbol"])
94
+ def case_sql_function() -> SymbolCase:
95
+ """CREATE FUNCTION — routine → function."""
96
+ src = """\
97
+ CREATE FUNCTION add(a INT, b INT) RETURNS INT
98
+ LANGUAGE SQL
99
+ AS $$ SELECT a + b; $$;
100
+ """
101
+ return "sql", src, [("function", "add", "")]
102
+
103
+
104
+ @case(tags=["symbol"])
105
+ def case_sql_select() -> SymbolCase:
106
+ """SELECT → function, named by its primary FROM table."""
107
+ return "sql", "SELECT id, name FROM users;\n", [("function", "users", "")]
108
+
109
+
110
+ @case(tags=["symbol"])
111
+ def case_sql_insert() -> SymbolCase:
112
+ """INSERT → function, named by its target table."""
113
+ return "sql", "INSERT INTO logs (msg) VALUES ('hi');\n", [("function", "logs", "")]
114
+
115
+
116
+ @case(tags=["symbol"])
117
+ def case_sql_update() -> SymbolCase:
118
+ """UPDATE → function, named by its target table."""
119
+ return "sql", "UPDATE users SET name = 'x' WHERE id = 1;\n", [("function", "users", "")]
120
+
121
+
122
+ @case(tags=["symbol"])
123
+ def case_sql_delete() -> SymbolCase:
124
+ """DELETE → function, named by its target table."""
125
+ return "sql", "DELETE FROM sessions WHERE id = 1;\n", [("function", "sessions", "")]
126
+
127
+
128
+ @case(tags=["symbol"])
129
+ def case_sql_cte() -> SymbolCase:
130
+ """Each CTE → a function named by its identifier."""
131
+ src = """\
132
+ WITH ranked AS (
133
+ SELECT id FROM events
134
+ ),
135
+ recent AS (
136
+ SELECT id FROM ranked
137
+ )
138
+ SELECT id FROM recent;
139
+ """
140
+ return "sql", src, [("function", "ranked", ""), ("function", "recent", "")]
141
+
142
+
143
+ # ── DDL operations: function (alter/drop) ────────────────────────────
144
+
145
+
146
+ @case(tags=["symbol"])
147
+ def case_sql_alter_table() -> SymbolCase:
148
+ """ALTER TABLE → function, named by the table."""
149
+ return "sql", "ALTER TABLE users ADD COLUMN age INT;\n", [("function", "users", "")]
150
+
151
+
152
+ @case(tags=["symbol"])
153
+ def case_sql_drop_table() -> SymbolCase:
154
+ """DROP TABLE → function, named by the table."""
155
+ return "sql", "DROP TABLE legacy;\n", [("function", "legacy", "")]
156
+
157
+
158
+ @case(tags=["symbol"])
159
+ def case_sql_drop_index() -> SymbolCase:
160
+ """DROP INDEX → function, named by the index."""
161
+ return "sql", "DROP INDEX idx_old;\n", [("function", "idx_old", "")]
162
+
163
+
164
+ # ── Naming invariances (optional clauses must not change the name) ───
165
+
166
+
167
+ @case(tags=["symbol"])
168
+ def case_sql_table_if_not_exists() -> SymbolCase:
169
+ """IF NOT EXISTS does not change the table name."""
170
+ return "sql", "CREATE TABLE IF NOT EXISTS users (id INT);\n", [("class", "users", "")]
171
+
172
+
173
+ @case(tags=["symbol"])
174
+ def case_sql_view_or_replace() -> SymbolCase:
175
+ """CREATE OR REPLACE does not change the view name."""
176
+ return "sql", "CREATE OR REPLACE VIEW v AS SELECT 1;\n", [("class", "v", "")]
177
+
178
+
179
+ @case(tags=["symbol"])
180
+ def case_sql_schema_qualified_table() -> SymbolCase:
181
+ """A schema-qualified table is named by the table, not the schema."""
182
+ return "sql", "CREATE TABLE app.users (id INT);\n", [("class", "users", "")]
183
+
184
+
185
+ @case(tags=["symbol"])
186
+ def case_sql_select_aliased_table() -> SymbolCase:
187
+ """An aliased FROM is named by the table, not the alias."""
188
+ src = "SELECT c.id FROM chunks AS c JOIN files f ON f.id = c.id;\n"
189
+ return "sql", src, [("function", "chunks", "")]
190
+
191
+
192
+ @case(tags=["symbol"])
193
+ def case_sql_create_table_as_select() -> SymbolCase:
194
+ """CREATE TABLE AS SELECT yields only the table, not the nested select."""
195
+ return "sql", "CREATE TABLE snap AS SELECT * FROM users;\n", [("class", "snap", "")]
196
+
197
+
198
+ @case(tags=["symbol"])
199
+ def case_sql_insert_select() -> SymbolCase:
200
+ """INSERT ... SELECT is named by the insert target."""
201
+ src = "INSERT INTO archive SELECT * FROM events;\n"
202
+ return "sql", src, [("function", "archive", "")]
203
+
204
+
205
+ # ── Unnamed statements (no nameable target) ──────────────────────────
206
+
207
+
208
+ @case(tags=["symbol"])
209
+ def case_sql_select_no_table() -> SymbolCase:
210
+ """A SELECT with no table has no name."""
211
+ return "sql", "SELECT 1;\n", [("function", "", "")]
212
+
213
+
214
+ @case(tags=["symbol"])
215
+ def case_sql_union() -> SymbolCase:
216
+ """A top-level UNION (set_operation) is one unnamed function chunk."""
217
+ src = "SELECT id FROM a UNION SELECT id FROM b;\n"
218
+ return "sql", src, [("function", "", "")]
219
+
220
+
221
+ @case(tags=["symbol"])
222
+ def case_sql_param_error_recovery() -> SymbolCase:
223
+ """A DuckDB $param errors internally but the statement still extracts.
224
+
225
+ The `$id` placeholder is unknown to the grammar (an ERROR node),
226
+ but error recovery keeps the enclosing statement, so the chunk is
227
+ still produced and named by its table.
228
+ """
229
+ return "sql", "SELECT * FROM users WHERE id = $id;\n", [("function", "users", "")]
230
+
231
+
232
+ # ── Mixed ────────────────────────────────────────────────────────────
233
+
234
+
235
+ @case(tags=["mixed"])
236
+ def case_sql_migration() -> MixedCase:
237
+ """Realistic migration script producing every SQL chunk kind.
238
+
239
+ SQL has no method scoping, so the expected-methods list is
240
+ empty — definitions are all top-level.
241
+ """
242
+ src = """\
243
+ CREATE SCHEMA shop;
244
+
245
+ CREATE TABLE shop.products (
246
+ id INT PRIMARY KEY,
247
+ name TEXT NOT NULL,
248
+ price NUMERIC
249
+ );
250
+
251
+ CREATE VIEW in_stock AS
252
+ SELECT * FROM shop.products WHERE price > 0;
253
+
254
+ CREATE FUNCTION price_with_tax(p NUMERIC) RETURNS NUMERIC
255
+ LANGUAGE SQL
256
+ AS $$ SELECT p * 1.2; $$;
257
+
258
+ CREATE INDEX idx_products_name ON shop.products (name);
259
+ """
260
+ return (
261
+ "sql",
262
+ src,
263
+ {"class", "function", "variable"},
264
+ [],
265
+ )
@@ -0,0 +1,38 @@
1
+ -- Greeter schema — tables, views, and statements for greetings.
2
+ --
3
+ -- The SQL plugin extracts one chunk per top-level statement: CREATE
4
+ -- TABLE/VIEW (as classes), CREATE FUNCTION and DML/CTEs (as functions),
5
+ -- and CREATE SCHEMA/INDEX/SEQUENCE (as variables). There is no import
6
+ -- concept. CREATE PROCEDURE and PRAGMA do not parse and live in the
7
+ -- xfail registry instead.
8
+
9
+ CREATE SCHEMA greet; -- trailing comment: its own chunk
10
+
11
+ -- Standalone note, separated by blank lines from any statement.
12
+ -- Second line of the same block.
13
+
14
+ CREATE TABLE greeters (
15
+ id INTEGER PRIMARY KEY,
16
+ prefix TEXT NOT NULL
17
+ );
18
+
19
+ CREATE INDEX idx_greeters_prefix ON greeters (prefix);
20
+
21
+ CREATE SEQUENCE greeting_id_seq;
22
+
23
+ CREATE VIEW active_greeters AS
24
+ SELECT id, prefix FROM greeters WHERE prefix IS NOT NULL;
25
+
26
+ CREATE FUNCTION shout(message TEXT) RETURNS TEXT AS 'SELECT upper(message)';
27
+
28
+ WITH recent AS (
29
+ SELECT id FROM greeters
30
+ )
31
+
32
+ SELECT id FROM recent;
33
+
34
+ INSERT INTO greeters (id, prefix) VALUES (1, 'Hello');
35
+
36
+ UPDATE greeters SET prefix = 'Hi' WHERE id = 1;
37
+
38
+ DELETE FROM greeters WHERE id = 1;
@@ -0,0 +1,17 @@
1
+ -- ClickHouse dialect sample — MergeTree engine, ORDER BY, special types.
2
+
3
+ CREATE TABLE events (
4
+ event_date Date,
5
+ user_id UInt64,
6
+ name LowCardinality(String),
7
+ props Array(String),
8
+ value Nullable(Float64)
9
+ )
10
+ ENGINE = MergeTree
11
+ PARTITION BY toYYYYMM(event_date)
12
+ ORDER BY (user_id, event_date);
13
+
14
+ CREATE MATERIALIZED VIEW events_mv
15
+ ENGINE = SummingMergeTree
16
+ ORDER BY user_id
17
+ AS SELECT user_id, count() AS c FROM events GROUP BY user_id;
@@ -0,0 +1,14 @@
1
+ -- DuckDB dialect sample — LIST/STRUCT types, CTAS, CREATE MACRO.
2
+
3
+ CREATE TABLE users (
4
+ id BIGINT PRIMARY KEY,
5
+ email VARCHAR NOT NULL,
6
+ tags VARCHAR[],
7
+ profile STRUCT(name VARCHAR, age INTEGER)
8
+ );
9
+
10
+ CREATE TABLE recent AS SELECT * FROM users LIMIT 10;
11
+
12
+ CREATE MACRO add_one(x) AS x + 1;
13
+
14
+ CREATE VIEW emails AS SELECT email FROM users;
@@ -0,0 +1,11 @@
1
+ -- MySQL dialect sample — backtick identifiers, UNSIGNED AUTO_INCREMENT, ENGINE.
2
+
3
+ CREATE TABLE `users` (
4
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
5
+ `email` VARCHAR(255) NOT NULL,
6
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
7
+ PRIMARY KEY (`id`),
8
+ UNIQUE KEY `uq_email` (`email`)
9
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
10
+
11
+ CREATE INDEX `idx_email` ON `users` (`email`);
@@ -0,0 +1,18 @@
1
+ -- PostgreSQL dialect sample — SERIAL, JSONB, arrays, ENUM type, plpgsql.
2
+
3
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
4
+
5
+ CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
6
+
7
+ CREATE TABLE users (
8
+ id SERIAL PRIMARY KEY,
9
+ email TEXT UNIQUE NOT NULL,
10
+ profile JSONB DEFAULT '{}'::jsonb,
11
+ tags TEXT[],
12
+ created_at TIMESTAMPTZ DEFAULT now()
13
+ );
14
+
15
+ CREATE FUNCTION greet(name TEXT) RETURNS TEXT
16
+ LANGUAGE sql AS $$ SELECT 'Hello, ' || name $$;
17
+
18
+ CREATE INDEX idx_users_email ON users (email);
@@ -0,0 +1,11 @@
1
+ -- SQLite dialect sample — AUTOINCREMENT, WITHOUT ROWID, IF NOT EXISTS.
2
+
3
+ CREATE TABLE IF NOT EXISTS users (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ email TEXT NOT NULL UNIQUE,
6
+ created_at TEXT DEFAULT (datetime('now'))
7
+ ) WITHOUT ROWID;
8
+
9
+ CREATE INDEX idx_users_email ON users (email);
10
+
11
+ CREATE VIEW active_users AS SELECT id, email FROM users;
@@ -0,0 +1,85 @@
1
+ """SQL extraction tests.
2
+
3
+ Construct/mixed cases (`cases_extraction.py`) drive the shared checks;
4
+ the two functions at the end pin SQL-specific edge behaviour.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+ from pytest_cases import parametrize_with_cases
11
+
12
+ from rbtr.domain.models import ChunkKind
13
+ from rbtr.git import FileEntry
14
+ from rbtr.languages.extract import extract_file
15
+
16
+
17
+ @parametrize_with_cases("lang, source, expected", cases=".cases_extraction", has_tag="symbol")
18
+ def test_extracts_expected_symbols(lang: str, source: str, expected: list) -> None:
19
+ """Each expected (kind, name, scope) tuple appears in the output."""
20
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
21
+ symbols = [(c.kind, c.name, c.scope) for c in chunks]
22
+ for exp in expected:
23
+ assert exp in symbols, f"expected {exp} not found in {symbols}"
24
+
25
+
26
+ @parametrize_with_cases(
27
+ "lang, source, expected_kinds, expected_methods", cases=".cases_extraction", has_tag="mixed"
28
+ )
29
+ def test_extracts_all_expected_kinds(
30
+ lang: str,
31
+ source: str,
32
+ expected_kinds: set[str],
33
+ expected_methods: list[tuple[str, str]],
34
+ ) -> None:
35
+ """Realistic source produces all expected chunk kinds and method scoping."""
36
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
37
+ kinds = {c.kind for c in chunks}
38
+ for kind in expected_kinds:
39
+ assert kind in kinds, f"expected kind {kind!r} not in {kinds}"
40
+ methods = [(c.name, c.scope) for c in chunks if c.kind == ChunkKind.METHOD]
41
+ for name, scope in expected_methods:
42
+ assert (name, scope) in methods, f"expected method ({name}, {scope}) not in {methods}"
43
+
44
+
45
+ def test_sql_pragma_not_extracted() -> None:
46
+ """A DuckDB PRAGMA yields no definition chunk.
47
+
48
+ The grammar has no PRAGMA statement node, so it parses to a top-level
49
+ ERROR with no enclosing `statement` to capture. This is a known
50
+ limitation guard; it flags the day the grammar gains PRAGMA support.
51
+ Only the content-less host-presence chunk (for blob dedup) remains.
52
+ """
53
+ src = "PRAGMA create_fts_index('chunks', 'id', 'body');\n"
54
+ chunks = extract_file(FileEntry("input", "sha1", src.encode()), "sql")
55
+ assert [c for c in chunks if c.content] == []
56
+
57
+
58
+ @pytest.mark.xfail(
59
+ strict=True,
60
+ reason="tree-sitter-sql has no create_procedure node — parses to ERROR",
61
+ )
62
+ def test_stored_procedure_unsupported() -> None:
63
+ """A SQL stored procedure should ideally extract as a function (grammar gap).
64
+
65
+ Strict xfail: when the grammar gains `create_procedure` this fails, prompting
66
+ removal of the xfail and a sample update.
67
+ """
68
+ src = "CREATE PROCEDURE refresh()\nLANGUAGE SQL\nAS $$ DELETE FROM cache; $$;\n"
69
+ chunks = extract_file(FileEntry("input", "sha1", src.encode()), "sql")
70
+ assert (ChunkKind.FUNCTION, "refresh", "") in [(c.kind, c.name, c.scope) for c in chunks]
71
+
72
+
73
+ def test_sql_multi_statement_one_chunk_each() -> None:
74
+ """Each top-level statement in a file becomes its own chunk."""
75
+ src = """\
76
+ CREATE TABLE a (id INT);
77
+ SELECT * FROM a;
78
+ DROP TABLE a;
79
+ """
80
+ chunks = extract_file(FileEntry("input", "sha1", src.encode()), "sql")
81
+ assert [(c.kind, c.name) for c in chunks] == [
82
+ (ChunkKind.CLASS, "a"),
83
+ (ChunkKind.FUNCTION, "a"),
84
+ (ChunkKind.FUNCTION, "a"),
85
+ ]