resolveWriteTargets returns the tables a DML/DDL statement writes/changes, using the same extractor plan-check trusts. Returns (nil, nil) when the engine has no extractor, the statement can't be parsed, or no targets are found — callers must then fall back to the database-level check (never fail ope
( ctx context.Context, instance *store.InstanceMessage, database *store.DatabaseMessage, statement string, requestSchema string, )
| 1915 | // records nothing and falls back to the request-database check. Object DDL the extractor does |
| 1916 | // not model still resolves to zero targets → request-database fallback. |
| 1917 | func (s *SQLService) resolveWriteTargets( |
| 1918 | ctx context.Context, |
| 1919 | instance *store.InstanceMessage, |
| 1920 | database *store.DatabaseMessage, |
| 1921 | statement string, |
| 1922 | requestSchema string, |
| 1923 | ) ([]writeTargetResource, error) { |
| 1924 | engine := instance.Metadata.GetEngine() |
| 1925 | |
| 1926 | stmts, err := parserbase.ParseStatements(engine, statement) |
| 1927 | if err != nil { |
| 1928 | //nolint:nilerr // unparseable here → fall back to database-level check (already parsed once for the span) |
| 1929 | return nil, nil |
| 1930 | } |
| 1931 | asts := parserbase.ExtractASTs(stmts) |
| 1932 | if len(asts) == 0 { |
| 1933 | return nil, nil |
| 1934 | } |
| 1935 | |
| 1936 | // Cache-first: GetQuerySpan already fetched and cached this database's metadata earlier in |
| 1937 | // the same request, so reuse it instead of issuing a second store query on the write path. |
| 1938 | dbMeta, err := s.store.GetDBSchemaSnapshot(ctx, common.GetWorkspaceIDFromContext(ctx), database.InstanceID, database.DatabaseName) |
| 1939 | if err != nil { |
| 1940 | return nil, err |
| 1941 | } |
| 1942 | if dbMeta == nil { |
| 1943 | return nil, nil |
| 1944 | } |
| 1945 | |
| 1946 | changeSummary, err := parserbase.ExtractChangedResources( |
| 1947 | engine, database.DatabaseName, schemaForWriteTargetResolution(engine, database.DatabaseName, requestSchema), dbMeta, asts, statement, |
| 1948 | ) |
| 1949 | if err != nil || changeSummary == nil || changeSummary.ChangedResources == nil { |
| 1950 | //nolint:nilerr // unsupported engine / no result → fall back to database-level check |
| 1951 | return nil, nil |
| 1952 | } |
| 1953 | |
| 1954 | var targets []writeTargetResource |
| 1955 | databasesWithTableTarget := map[string]bool{} |
| 1956 | for _, db := range changeSummary.ChangedResources.Build().GetDatabases() { |
| 1957 | for _, sc := range db.GetSchemas() { |
| 1958 | schema := sc.GetName() |
| 1959 | // An unqualified target whose executed schema can't be determined here resolves |
| 1960 | // to the sentinel; treat it as unknown ("") so the schema_name attribute is |
| 1961 | // omitted (fail-closed for schema-scoped grants, still matches table-only ones). |
| 1962 | if schema == unresolvedSchemaSentinel { |
| 1963 | schema = "" |
| 1964 | } |
| 1965 | for _, tbl := range sc.GetTables() { |
| 1966 | databasesWithTableTarget[db.GetName()] = true |
| 1967 | targets = append(targets, writeTargetResource{ |
| 1968 | database: db.GetName(), |
| 1969 | schema: schema, |
| 1970 | table: tbl.GetName(), |
| 1971 | }) |
| 1972 | } |
| 1973 | } |
| 1974 | } |
no test coverage detected