Skip to content

Commit a5189ea

Browse files
authored
Merge pull request github#8735 from redsun82/swift-dbscheme-gen
Swift: dbscheme generator
2 parents 813de65 + 24697fe commit a5189ea

16 files changed

+3420
-9
lines changed

.pre-commit-config.yaml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,41 @@
22
# See https://pre-commit.com/hooks.html for more hooks
33
exclude: /test/.*$(?<!\.ql)(?<!\.qll)(?<!\.qlref)
44
repos:
5-
- repo: https://github.com/pre-commit/pre-commit-hooks
5+
- repo: https://github.com/pre-commit/pre-commit-hooks
66
rev: v3.2.0
77
hooks:
8-
- id: trailing-whitespace
9-
- id: end-of-file-fixer
8+
- id: trailing-whitespace
9+
- id: end-of-file-fixer
1010

11-
- repo: local
11+
- repo: https://github.com/pre-commit/mirrors-clang-format
12+
rev: v13.0.1
1213
hooks:
13-
- id: codeql-format
14+
- id: clang-format
15+
files: ^swift/.*\.(h|c|cpp)$
16+
17+
- repo: local
18+
hooks:
19+
- id: codeql-format
1420
name: Fix QL file formatting
1521
files: \.qll?$
1622
language: system
1723
entry: codeql query format --in-place
1824

19-
- id: sync-files
25+
- id: sync-files
2026
name: Fix files required to be identical
2127
language: system
2228
entry: python3 config/sync-files.py --latest
2329
pass_filenames: false
2430

25-
- id: qhelp
31+
- id: qhelp
2632
name: Check query help generation
2733
files: \.qhelp$
2834
language: system
2935
entry: python3 misc/scripts/check-qhelp.py
36+
37+
- id: swift-codegen
38+
name: Run Swift checked in code generation
39+
files: ^swift/(codegen/|.*/generated/|ql/lib/swift\.dbscheme$)
40+
language: system
41+
entry: bazel run //swift/codegen
42+
pass_filenames: false

swift/README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ The Swift codeql package is an experimental and unsupported work in progress.
44

55
## Usage
66

7-
Run `bazel run //swift:create-extractor-pack`, which will install `swift/extractor-pack`.
7+
Run
8+
9+
```bash
10+
bazel run //swift:create-extractor-pack
11+
```
12+
13+
which will install `swift/extractor-pack`.
14+
815
Using `--search-path=swift/extractor-pack` will then pick up the Swift extractor. You can also use
916
`--search-path=swift`, as the extractor pack is mentioned in `swift/.codeqlmanifest.json`.
17+
18+
Notice you can run `bazel run :create-extractor-pack` if you already are in the `swift` directory.
19+
20+
## Code generation
21+
22+
Make sure to install the [pip requirements](./codegen/requirements.txt) via
23+
24+
```bash
25+
python3 -m pip install -r codegen/requirements.txt
26+
```
27+
28+
Run
29+
30+
```bash
31+
bazel run //swift/codegen
32+
```
33+
34+
to update generated files. This can be shortened to
35+
`bazel run codegen` if you are in the `swift` directory.

swift/codegen/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
py_binary(
2+
name = "codegen",
3+
srcs = glob(["**/*.py"]),
4+
)

swift/codegen/codegen.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env python3
2+
""" Driver script to run all checked in code generation """
3+
4+
from lib import generator
5+
import dbschemegen
6+
7+
if __name__ == "__main__":
8+
generator.run(dbschemegen.generate)

swift/codegen/dbschemegen.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
import pathlib
3+
4+
import inflection
5+
6+
from lib import paths, schema, generator
7+
from lib.dbscheme import *
8+
9+
log = logging.getLogger(__name__)
10+
11+
12+
def dbtype(typename):
13+
""" translate a type to a dbscheme counterpart, using `@lower_underscore` format for classes """
14+
if typename[0].isupper():
15+
return "@" + inflection.underscore(typename)
16+
return typename
17+
18+
19+
def cls_to_dbscheme(cls: schema.Class):
20+
""" Yield all dbscheme entities needed to model class `cls` """
21+
if cls.derived:
22+
yield DbUnion(dbtype(cls.name), (dbtype(c) for c in cls.derived))
23+
# output a table specific to a class only if it is a leaf class or it has 1-to-1 properties
24+
# Leaf classes need a table to bind the `@` ids
25+
# 1-to-1 properties are added to a class specific table
26+
# in other cases, separate tables are used for the properties, and a class specific table is unneeded
27+
if not cls.derived or any(f.is_single for f in cls.properties):
28+
binding = not cls.derived
29+
keyset = DbKeySet(["id"]) if cls.derived else None
30+
yield DbTable(
31+
keyset=keyset,
32+
name=inflection.tableize(cls.name),
33+
columns=[
34+
DbColumn("id", type=dbtype(cls.name), binding=binding),
35+
] + [
36+
DbColumn(f.name, dbtype(f.type)) for f in cls.properties if f.is_single
37+
]
38+
)
39+
# use property-specific tables for 1-to-many and 1-to-at-most-1 properties
40+
for f in cls.properties:
41+
if f.is_optional:
42+
yield DbTable(
43+
keyset=DbKeySet(["id"]),
44+
name=inflection.tableize(f"{cls.name}_{f.name}"),
45+
columns=[
46+
DbColumn("id", type=dbtype(cls.name)),
47+
DbColumn(f.name, dbtype(f.type)),
48+
],
49+
)
50+
elif f.is_repeated:
51+
yield DbTable(
52+
keyset=DbKeySet(["id", "index"]),
53+
name=inflection.tableize(f"{cls.name}_{f.name}"),
54+
columns=[
55+
DbColumn("id", type=dbtype(cls.name)),
56+
DbColumn("index", type="int"),
57+
DbColumn(inflection.singularize(f.name), dbtype(f.type)),
58+
]
59+
)
60+
61+
62+
def get_declarations(data: schema.Schema):
63+
return [d for cls in data.classes.values() for d in cls_to_dbscheme(cls)]
64+
65+
66+
def get_includes(data: schema.Schema, include_dir: pathlib.Path):
67+
includes = []
68+
for inc in data.includes:
69+
inc = include_dir / inc
70+
with open(inc) as inclusion:
71+
includes.append(DbSchemeInclude(src=inc.relative_to(paths.swift_dir), data=inclusion.read()))
72+
return includes
73+
74+
75+
def generate(opts, renderer):
76+
input = opts.schema.resolve()
77+
out = opts.dbscheme.resolve()
78+
79+
with open(input) as src:
80+
data = schema.load(src)
81+
82+
dbscheme = DbScheme(src=input.relative_to(paths.swift_dir),
83+
includes=get_includes(data, include_dir=input.parent),
84+
declarations=get_declarations(data))
85+
86+
renderer.render(dbscheme, out)
87+
88+
89+
if __name__ == "__main__":
90+
generator.run(generate, tags=["schema", "dbscheme"])

swift/codegen/lib/dbscheme.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
""" dbscheme format representation """
2+
3+
import logging
4+
from dataclasses import dataclass
5+
from typing import ClassVar, List
6+
7+
log = logging.getLogger(__name__)
8+
9+
dbscheme_keywords = {"case", "boolean", "int", "string", "type"}
10+
11+
12+
@dataclass
13+
class DbColumn:
14+
schema_name: str
15+
type: str
16+
binding: bool = False
17+
first: bool = False
18+
19+
@property
20+
def name(self):
21+
if self.schema_name in dbscheme_keywords:
22+
return self.schema_name + "_"
23+
return self.schema_name
24+
25+
@property
26+
def lhstype(self):
27+
if self.type[0] == "@":
28+
return "unique int" if self.binding else "int"
29+
return self.type
30+
31+
@property
32+
def rhstype(self):
33+
if self.type[0] == "@" and self.binding:
34+
return self.type
35+
return self.type + " ref"
36+
37+
38+
@dataclass
39+
class DbKeySetId:
40+
id: str
41+
first: bool = False
42+
43+
44+
@dataclass
45+
class DbKeySet:
46+
ids: List[DbKeySetId]
47+
48+
def __post_init__(self):
49+
assert self.ids
50+
self.ids = [DbKeySetId(x) for x in self.ids]
51+
self.ids[0].first = True
52+
53+
54+
class DbDecl:
55+
is_table = False
56+
is_union = False
57+
58+
59+
@dataclass
60+
class DbTable(DbDecl):
61+
is_table: ClassVar = True
62+
63+
name: str
64+
columns: List[DbColumn]
65+
keyset: DbKeySet = None
66+
67+
def __post_init__(self):
68+
if self.columns:
69+
self.columns[0].first = True
70+
71+
72+
@dataclass
73+
class DbUnionCase:
74+
type: str
75+
first: bool = False
76+
77+
78+
@dataclass
79+
class DbUnion(DbDecl):
80+
is_union: ClassVar = True
81+
82+
lhs: str
83+
rhs: List[DbUnionCase]
84+
85+
def __post_init__(self):
86+
assert self.rhs
87+
self.rhs = [DbUnionCase(x) for x in self.rhs]
88+
self.rhs.sort(key=lambda c: c.type)
89+
self.rhs[0].first = True
90+
91+
92+
@dataclass
93+
class DbSchemeInclude:
94+
src: str
95+
data: str
96+
97+
98+
@dataclass
99+
class DbScheme:
100+
template: ClassVar = 'dbscheme'
101+
102+
src: str
103+
includes: List[DbSchemeInclude]
104+
declarations: List[DbDecl]

swift/codegen/lib/generator.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
""" generator script scaffolding """
2+
3+
import argparse
4+
import logging
5+
import sys
6+
7+
from . import options, render
8+
9+
10+
def _parse(tags):
11+
parser = argparse.ArgumentParser()
12+
for opt in options.get(tags):
13+
opt.add_to(parser)
14+
ret = parser.parse_args()
15+
log_level = logging.DEBUG if ret.verbose else logging.INFO
16+
logging.basicConfig(format="{levelname} {message}", style='{', level=log_level)
17+
return ret
18+
19+
20+
def run(*generators, tags=None):
21+
""" run generation functions in `generators`, parsing options tagged with `tags` (all if unspecified)
22+
23+
`generators` should be callables taking as input an option namespace and a `render.Renderer` instance
24+
"""
25+
opts = _parse(tags)
26+
renderer = render.Renderer(dryrun=opts.check)
27+
for g in generators:
28+
g(opts, renderer)
29+
sys.exit(1 if opts.check and renderer.done_something else 0)

swift/codegen/lib/options.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
""" generator options, categorized by tags """
2+
3+
import argparse
4+
import collections
5+
import pathlib
6+
from typing import Tuple
7+
8+
from . import paths
9+
10+
11+
def _init_options():
12+
Option("--check", "-c", action="store_true")
13+
Option("--verbose", "-v", action="store_true")
14+
Option("--schema", tags=["schema"], type=pathlib.Path, default=paths.swift_dir / "codegen/schema.yml")
15+
Option("--dbscheme", tags=["dbscheme"], type=pathlib.Path, default=paths.swift_dir / "ql/lib/swift.dbscheme")
16+
17+
18+
_options = collections.defaultdict(list)
19+
20+
21+
class Option:
22+
def __init__(self, *args, tags=None, **kwargs):
23+
tags = tags or []
24+
self.args = args
25+
self.kwargs = kwargs
26+
if tags:
27+
for t in tags:
28+
_options[t].append(self)
29+
else:
30+
_options["*"].append(self)
31+
32+
def add_to(self, parser: argparse.ArgumentParser):
33+
parser.add_argument(*self.args, **self.kwargs)
34+
35+
36+
_init_options()
37+
38+
39+
def get(tags: Tuple[str]):
40+
""" get options marked by `tags`
41+
42+
Return all options if tags is falsy. Options tagged by wildcard '*' are always returned
43+
"""
44+
if not tags:
45+
return (o for tagged_opts in _options.values() for o in tagged_opts)
46+
else:
47+
# use specifically tagged options + those tagged with wildcard *
48+
return (o for tag in ('*',) + tags for o in _options[tag])

swift/codegen/lib/paths.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
""" module providing useful filesystem paths """
2+
3+
import pathlib
4+
import sys
5+
import os
6+
7+
try:
8+
_workspace_dir = pathlib.Path(os.environ['BUILD_WORKSPACE_DIRECTORY']) # <- means we are using bazel run
9+
swift_dir = _workspace_dir / 'swift'
10+
lib_dir = swift_dir / 'codegen' / 'lib'
11+
except KeyError:
12+
_this_file = pathlib.Path(__file__).resolve()
13+
swift_dir = _this_file.parents[2]
14+
lib_dir = _this_file.parent
15+
16+
17+
exe_file = pathlib.Path(sys.argv[0]).resolve()

0 commit comments

Comments
 (0)