gh-133390: sqlite3 CLI completion for tables, columns, indices, trigg… · python/cpython@161b306

GitHub

GitHub CopilotWrite better code with AI | MCP RegistryIntegrate external tools | ActionsAutomate any workflow | CodespacesInstant dev environments | IssuesPlan and track work | Code ReviewManage code changes | Code QualityEnforce quality at merge | Why GitHub | Marketplace | View all features | Enterprises | Small and medium teams | Startups | View all use cases | View all industries | View all solutions | AI | Software Development | DevOps | Security | View all topics | Customer stories | Events & webinars | Ebooks & reports | Business insights | Trust center | Partners | View all resources

@@ -216,10 +216,6 @@ class Completion(unittest.TestCase):

216216217217@classmethod

218218defsetUpClass(cls):

219-_sqlite3=import_module("_sqlite3")

220-ifnothasattr(_sqlite3, "SQLITE_KEYWORDS"):

221-raiseunittest.SkipTest("unable to determine SQLite keywords")

222-223219readline=import_module("readline")

224220ifreadline.backend=="editline":

225221raiseunittest.SkipTest("libedit readline is not supported")

@@ -229,12 +225,24 @@ def write_input(self, input_, env=None):

229225 import readline

230226 from sqlite3.__main__ import main

231227228+ # Configure readline to ...:

229+ # - hide control sequences surrounding each candidate

230+ # - hide "Display all xxx possibilities? (y or n)"

231+ # - show candidates one per line

232232 readline.parse_and_bind("set colored-completion-prefix off")

233+ readline.parse_and_bind("set completion-query-items 0")

234+ readline.parse_and_bind("set page-completions off")

235+ readline.parse_and_bind("set completion-display-width 0")

236+233237 main()

234238 """)

235239returnrun_pty(script, input_, env)

236240237241deftest_complete_sql_keywords(self):

242+_sqlite3=import_module("_sqlite3")

243+ifnothasattr(_sqlite3, "SQLITE_KEYWORDS"):

244+raiseunittest.SkipTest("unable to determine SQLite keywords")

245+238246# List candidates starting with 'S', there should be multiple matches.

239247input_=b"S\t\tEL\t 1;\n.quit\n"

240248output=self.write_input(input_)

@@ -254,6 +262,118 @@ def test_complete_sql_keywords(self):

254262output=self.write_input(input_)

255263self.assertIn(b".version", output)

256264265+deftest_complete_table_indexes_triggers_views(self):

266+input_=textwrap.dedent("""\

267+ CREATE TABLE _Table (id);

268+ CREATE INDEX _Index ON _table (id);

269+ CREATE TRIGGER _Trigger BEFORE INSERT

270+ ON _Table BEGIN SELECT 1; END;

271+ CREATE VIEW _View AS SELECT 1;

272+273+ CREATE TEMP TABLE _Temp_table (id);

274+ CREATE INDEX temp._Temp_index ON _Temp_table (id);

275+ CREATE TEMP TRIGGER _Temp_trigger BEFORE INSERT

276+ ON _Table BEGIN SELECT 1; END;

277+ CREATE TEMP VIEW _Temp_view AS SELECT 1;

278+279+ ATTACH ':memory:' AS attached;

280+ CREATE TABLE attached._Attached_table (id);

281+ CREATE INDEX attached._Attached_index ON _Attached_table (id);

282+ CREATE TRIGGER attached._Attached_trigger BEFORE INSERT

283+ ON _Attached_table BEGIN SELECT 1; END;

284+ CREATE VIEW attached._Attached_view AS SELECT 1;

285+286+ SELECT id FROM _\t\tta\t;

287+ .quit\n""").encode()

288+output=self.write_input(input_)

289+lines=output.decode().splitlines()

290+indices= [ifori, lineinenumerate(lines)

291+ifline.startswith(self.PS1)]

292+start, end=indices[-3], indices[-2]

293+candidates= [l.strip() forlinlines[start+1:end]]

294+self.assertEqual(candidates,

295+ [

296+"_Attached_index",

297+"_Attached_table",

298+"_Attached_trigger",

299+"_Attached_view",

300+"_Index",

301+"_Table",

302+"_Temp_index",

303+"_Temp_table",

304+"_Temp_trigger",

305+"_Temp_view",

306+"_Trigger",

307+"_View",

308+ ],

309+ )

310+start, end=indices[-2], indices[-1]

311+# direct match with '_Table' completed, no candidates displayed

312+candidates= [l.strip() forlinlines[start+1:end]]

313+self.assertEqual(len(candidates), 0)

[email protected](sqlite3.sqlite_version_info< (3, 16, 0),

316+"PRAGMA table-valued function is not available until "

317+"SQLite 3.16.0")

318+deftest_complete_columns(self):

319+input_=textwrap.dedent("""\

320+ CREATE TABLE _table (_col_table);

321+ CREATE TEMP TABLE _temp_table (_col_temp);

322+ ATTACH ':memory:' AS attached;

323+ CREATE TABLE attached._attached_table (_col_attached);

324+325+ SELECT _col_\t\tta\tFROM _table;

326+ .quit\n""").encode()

327+output=self.write_input(input_)

328+lines=output.decode().splitlines()

329+indices= [

330+ifori, lineinenumerate(lines) ifline.startswith(self.PS1)

331+ ]

332+start, end=indices[-3], indices[-2]

333+candidates= [l.strip() forlinlines[start+1:end]]

334+335+self.assertEqual(

336+candidates, ["_col_attached", "_col_table", "_col_temp"]

337+ )

[email protected](sqlite3.sqlite_version_info< (3, 30, 0),

340+"PRAGMA function_list is not available until "

341+"SQLite 3.30.0")

342+deftest_complete_functions(self):

343+input_=b"SELECT AV\t1);\n.quit\n"

344+output=self.write_input(input_)

345+self.assertIn(b"AVG(1);", output)

346+self.assertIn(b"(1.0,)", output)

347+348+# Functions are completed in upper case for even lower case user input.

349+input_=b"SELECT av\t1);\n.quit\n"

350+output=self.write_input(input_)

351+self.assertIn(b"AVG(1);", output)

352+self.assertIn(b"(1.0,)", output)

353+354+deftest_complete_schemata(self):

355+input_=textwrap.dedent("""\

356+ ATTACH ':memory:' AS MixedCase;

357+ -- Test '_' is escaped in Like pattern filtering

358+ ATTACH ':memory:' AS _underscore;

359+ -- Let database_list pragma have a 'temp' schema entry

360+ CREATE TEMP TABLE _table (id);

361+362+ SELECT * FROM \t\tmIX\t.sqlite_master;

363+ SELECT * FROM _und\t.sqlite_master;

364+ .quit\n""").encode()

365+output=self.write_input(input_)

366+lines=output.decode().splitlines()

367+indices= [

368+ifori, lineinenumerate(lines) ifline.startswith(self.PS1)

369+ ]

370+start, end=indices[-4], indices[-3]

371+candidates= [l.strip() forlinlines[start+1:end]]

372+self.assertIn("MixedCase", candidates)

373+self.assertIn("_underscore", candidates)

374+self.assertIn("main", candidates)

375+self.assertIn("temp", candidates)

[email protected](sys.platform.startswith("freebsd"),

258378"Two actual tabs are inserted when there are no matching"

259379" completions in the pseudo-terminal opened by run_pty()"

@@ -274,8 +394,6 @@ def test_complete_no_match(self):

274394self.assertEqual(line_num, len(lines))

275395276396deftest_complete_no_input(self):

277-from_sqlite3importSQLITE_KEYWORDS

278-279397script=textwrap.dedent("""

280398 import readline

281399 from sqlite3.__main__ import main

@@ -306,7 +424,7 @@ def test_complete_no_input(self):

306424self.assertEqual(len(indices), 2)

307425start, end=indices

308426candidates= [l.strip() forlinlines[start+1:end]]

309-self.assertEqual(candidates, sorted(SQLITE_KEYWORDS))

427+self.assertEqual(candidates, sorted(candidates))

310428except:

311429ifverbose:

312430print(' PTY output: '.center(30, '-'))